docs: add edit book actions implementation plan
This commit is contained in:
@@ -0,0 +1,600 @@
|
|||||||
|
# Edit Book Actions Implementation Plan
|
||||||
|
|
||||||
|
**Goal:** Allow users to change a book's status, add reading notes, and set ratings
|
||||||
|
from the agenda view via context-sensitive minibuffer actions.
|
||||||
|
|
||||||
|
**Architecture:** New Org write functions (`spine-set-status`, `spine-add-note`,
|
||||||
|
`spine-set-rating`) follow the same `find-file-noselect` + `save-buffer` + `kill-buffer`
|
||||||
|
pattern as `spine-add-book`. A single `/edit` handler dispatches based on `action`
|
||||||
|
query param. The index model gains a `minibuffer` field with action links computed
|
||||||
|
from the selected book's status.
|
||||||
|
|
||||||
|
**Tech Stack:** Emacs Lisp (backend + model), Mustache (template)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File overview
|
||||||
|
|
||||||
|
**Modify:**
|
||||||
|
- `spine.el` — 3 new write functions + `httpd/edit` handler + minibuffer in model
|
||||||
|
- `templates/index.mustache` — minibuffer renders action links
|
||||||
|
|
||||||
|
**Create:**
|
||||||
|
- `test/spine-edit-test.el` — tests for write functions + model minibuffer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Write failing tests for Org write functions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `test/spine-edit-test.el`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create test file with setup**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
;;; spine-edit-test.el — ERT tests for spine-edit functions
|
||||||
|
|
||||||
|
(require 'ert)
|
||||||
|
(require 'cl-lib)
|
||||||
|
|
||||||
|
(setq spine-skip-server-start t)
|
||||||
|
(setq spine-org-file (expand-file-name "test-edit-temp.org"
|
||||||
|
(file-name-directory
|
||||||
|
(directory-file-name
|
||||||
|
(file-name-directory load-file-name)))))
|
||||||
|
(load-file (expand-file-name "../spine.el"
|
||||||
|
(file-name-directory load-file-name)))
|
||||||
|
|
||||||
|
(defun spine-edit-test--setup ()
|
||||||
|
"Create a test Org file with one WANT book."
|
||||||
|
(spine-add-book :title "Edit Test Book" :author "Test Author"))
|
||||||
|
|
||||||
|
(defun spine-edit-test--cleanup ()
|
||||||
|
"Remove the test Org file."
|
||||||
|
(ignore-errors (delete-file spine-org-file)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test spine-set-status transitions WANT to READING**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-set-status-want-to-reading ()
|
||||||
|
"spine-set-status changes TODO from WANT to READING."
|
||||||
|
(spine-edit-test--cleanup)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(spine-edit-test--setup)
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (car books))
|
||||||
|
(id (plist-get book :id)))
|
||||||
|
(spine-set-status id "READING")
|
||||||
|
(let ((updated-books (spine-books))
|
||||||
|
(updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||||
|
(should updated)
|
||||||
|
(should (equal (plist-get updated :status) "READING")))))
|
||||||
|
(spine-edit-test--cleanup)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test spine-set-status transitions READING to READ**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-set-status-reading-to-read ()
|
||||||
|
"spine-set-status changes TODO from READING to READ."
|
||||||
|
(spine-edit-test--cleanup)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(spine-edit-test--setup)
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (car books))
|
||||||
|
(id (plist-get book :id)))
|
||||||
|
(spine-set-status id "READING")
|
||||||
|
(spine-set-status id "READ")
|
||||||
|
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||||
|
(should updated)
|
||||||
|
(should (equal (plist-get updated :status) "READ")))))
|
||||||
|
(spine-edit-test--cleanup)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test spine-add-note appends a note**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-add-note-appends ()
|
||||||
|
"spine-add-note appends a note with date and text."
|
||||||
|
(spine-edit-test--cleanup)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(spine-edit-test--setup)
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (car books))
|
||||||
|
(id (plist-get book :id)))
|
||||||
|
(spine-add-note id "Great book!" "2026-06-21")
|
||||||
|
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||||
|
(should updated)
|
||||||
|
(let ((notes (plist-get updated :notes)))
|
||||||
|
(should notes)
|
||||||
|
(should (equal (caar notes) "2026-06-21"))
|
||||||
|
(should (equal (cadar notes) "Great book!"))))))
|
||||||
|
(spine-edit-test--cleanup)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Test spine-add-note defaults to today's date**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-add-note-default-date ()
|
||||||
|
"spine-add-note uses today's date when none provided."
|
||||||
|
(spine-edit-test--cleanup)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(spine-edit-test--setup)
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (car books))
|
||||||
|
(id (plist-get book :id))
|
||||||
|
(today (format-time-string "%Y-%m-%d")))
|
||||||
|
(spine-add-note id "Note without date")
|
||||||
|
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||||
|
(should updated)
|
||||||
|
(let ((notes (plist-get updated :notes)))
|
||||||
|
(should notes)
|
||||||
|
(should (equal (caar notes) today))
|
||||||
|
(should (equal (cadar notes) "Note without date"))))))
|
||||||
|
(spine-edit-test--cleanup)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Test spine-set-rating**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-set-rating-sets-rating ()
|
||||||
|
"spine-set-rating sets the RATING property."
|
||||||
|
(spine-edit-test--cleanup)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(spine-edit-test--setup)
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (car books))
|
||||||
|
(id (plist-get book :id)))
|
||||||
|
(spine-set-rating id "4")
|
||||||
|
(let ((updated (cl-find id (spine-books) :key (lambda (b) (plist-get b :id)) :test #'equal)))
|
||||||
|
(should updated)
|
||||||
|
(should (equal (plist-get updated :rating) "4")))))
|
||||||
|
(spine-edit-test--cleanup)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Test spine-set-status with missing ID signals error**
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-set-status-missing-id ()
|
||||||
|
"spine-set-status signals error for unknown ID."
|
||||||
|
(should-error (spine-set-status "nonexistent-id" "READING")))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run tests to verify they fail**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-edit-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests fail because `spine-set-status` etc. are not defined yet.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit test file**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add test/spine-edit-test.el
|
||||||
|
git commit -m "test: add failing tests for spine-set-status, spine-add-note, spine-set-rating"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Implement Org write functions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `spine.el` (insert after `spine-add-book`, before `spine--extract-notes`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add spine-set-status**
|
||||||
|
|
||||||
|
Insert after line 251 (end of `spine-add-book`):
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun spine-set-status (id status)
|
||||||
|
"Set the TODO state of book with ID to STATUS.
|
||||||
|
STATUS should be one of: WANT, READING, READ, ABANDONED.
|
||||||
|
Signals an error if the book is not found."
|
||||||
|
(let ((org-file (expand-file-name spine-org-file)))
|
||||||
|
(unless (file-exists-p org-file)
|
||||||
|
(error "spine-set-status: file not found: %s" org-file))
|
||||||
|
(with-current-buffer (find-file-noselect org-file)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||||
|
(lambda (hl)
|
||||||
|
(when (= (org-element-property :level hl) 1)
|
||||||
|
(let ((pos (org-element-property :begin hl)))
|
||||||
|
(when (equal id (spine--prop pos "ID"))
|
||||||
|
(goto-char pos)
|
||||||
|
(org-todo status)))))
|
||||||
|
nil 'headline)
|
||||||
|
(unless (eq (point) (point-min))
|
||||||
|
(save-buffer)
|
||||||
|
t)
|
||||||
|
(when (eq (point) (point-min))
|
||||||
|
(error "spine-set-status: no book found with ID %s" id)))
|
||||||
|
(kill-buffer)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run tests to verify spine-set-status passes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-edit-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: spine-set-status tests pass, spine-add-note and spine-set-rating still fail.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add spine-add-note**
|
||||||
|
|
||||||
|
Insert after `spine-set-status`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun spine-add-note (id text &optional date)
|
||||||
|
"Append a reading note to the book with ID.
|
||||||
|
TEXT is the note content. DATE is an optional YYYY-MM-DD string
|
||||||
|
\(defaults to today). Signals an error if the book is not found."
|
||||||
|
(let ((org-file (expand-file-name spine-org-file))
|
||||||
|
(date-str (or date (format-time-string "%Y-%m-%d"))))
|
||||||
|
(unless (file-exists-p org-file)
|
||||||
|
(error "spine-add-note: file not found: %s" org-file))
|
||||||
|
(with-current-buffer (find-file-noselect org-file)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||||
|
(lambda (hl)
|
||||||
|
(when (= (org-element-property :level hl) 1)
|
||||||
|
(let ((pos (org-element-property :begin hl)))
|
||||||
|
(when (equal id (spine--prop pos "ID"))
|
||||||
|
(goto-char (org-entry-end-position))
|
||||||
|
(insert (format "\n- [%s] %s\n" date-str text))))))
|
||||||
|
nil 'headline)
|
||||||
|
(unless (eq (point) (point-min))
|
||||||
|
(save-buffer)
|
||||||
|
t)
|
||||||
|
(when (eq (point) (point-min))
|
||||||
|
(error "spine-add-note: no book found with ID %s" id)))
|
||||||
|
(kill-buffer)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run tests to verify spine-add-note passes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-edit-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: spine-set-status and spine-add-note tests pass, spine-set-rating still fails.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add spine-set-rating**
|
||||||
|
|
||||||
|
Insert after `spine-add-note`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun spine-set-rating (id rating)
|
||||||
|
"Set the RATING property of book with ID to RATING (1-5 string).
|
||||||
|
Signals an error if the book is not found."
|
||||||
|
(let ((org-file (expand-file-name spine-org-file)))
|
||||||
|
(unless (file-exists-p org-file)
|
||||||
|
(error "spine-set-rating: file not found: %s" org-file))
|
||||||
|
(with-current-buffer (find-file-noselect org-file)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||||
|
(lambda (hl)
|
||||||
|
(when (= (org-element-property :level hl) 1)
|
||||||
|
(let ((pos (org-element-property :begin hl)))
|
||||||
|
(when (equal id (spine--prop pos "ID"))
|
||||||
|
(goto-char pos)
|
||||||
|
(org-set-property "RATING" rating)))))
|
||||||
|
nil 'headline)
|
||||||
|
(unless (eq (point) (point-min))
|
||||||
|
(save-buffer)
|
||||||
|
t)
|
||||||
|
(when (eq (point) (point-min))
|
||||||
|
(error "spine-set-rating: no book found with ID %s" id)))
|
||||||
|
(kill-buffer)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run all edit tests to verify they pass**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-edit-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all 6 tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run existing tests to confirm nothing broke**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-index-model-test.el \
|
||||||
|
-l test/spine-books-test.el \
|
||||||
|
-l test/spine-add-book-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add spine.el
|
||||||
|
git commit -m "feat: add spine-set-status, spine-add-note, spine-set-rating"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Extend spine-index-model with minibuffer actions
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `spine.el` — `spine-index-model` function
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add minibuffer computation to per-book model**
|
||||||
|
|
||||||
|
In the per-book model building block (around line 158, after `"selected" selected`), add the minibuffer actions when the book is selected. Insert after the `detail` ht block and before closing the model ht:
|
||||||
|
|
||||||
|
Inside the `(let* (... (model (ht ...)))` block, after the `"detail"` key-value pair and before the closing `)))` of the let*, add:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
("minibuffer"
|
||||||
|
(when selected
|
||||||
|
(let ((status (plist-get book :status)))
|
||||||
|
(ht ("actions"
|
||||||
|
(append
|
||||||
|
(cond
|
||||||
|
((equal status "WANT")
|
||||||
|
(list
|
||||||
|
(ht ("command" "status") ("label" "Mark reading")
|
||||||
|
("href" (format "/edit?id=%s&action=status&value=READING" id)))
|
||||||
|
(ht ("command" "note") ("label" "Add note")
|
||||||
|
("href" (format "/edit?id=%s&action=note" id)))))
|
||||||
|
((equal status "READING")
|
||||||
|
(list
|
||||||
|
(ht ("command" "status") ("label" "Mark read")
|
||||||
|
("href" (format "/edit?id=%s&action=status&value=READ" id)))
|
||||||
|
(ht ("command" "status") ("label" "Abandon")
|
||||||
|
("href" (format "/edit?id=%s&action=status&value=ABANDONED" id)))
|
||||||
|
(ht ("command" "note") ("label" "Add note")
|
||||||
|
("href" (format "/edit?id=%s&action=note" id)))))
|
||||||
|
((equal status "READ")
|
||||||
|
(list
|
||||||
|
(ht ("command" "status") ("label" "Read again")
|
||||||
|
("href" (format "/edit?id=%s&action=status&value=READING" id)))
|
||||||
|
(ht ("command" "note") ("label" "Add note")
|
||||||
|
("href" (format "/edit?id=%s&action=note" id)))))
|
||||||
|
((equal status "ABANDONED")
|
||||||
|
(list
|
||||||
|
(ht ("command" "status") ("label" "Try again")
|
||||||
|
("href" (format "/edit?id=%s&action=status&value=READING" id)))))
|
||||||
|
(t nil))
|
||||||
|
nil))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write a test for minibuffer model**
|
||||||
|
|
||||||
|
Append to `test/spine-edit-test.el`:
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(ert-deftest spine-edit-model-minibuffer-want ()
|
||||||
|
"Selected WANT book has Mark reading and Add note actions."
|
||||||
|
(spine-edit-test--cleanup)
|
||||||
|
(unwind-protect
|
||||||
|
(progn
|
||||||
|
(spine-edit-test--setup)
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (car books))
|
||||||
|
(id (plist-get book :id))
|
||||||
|
(model (spine-index-model books nil id))
|
||||||
|
(groups (ht-get model "groups")))
|
||||||
|
(cl-labels ((find-book-model (groups id)
|
||||||
|
(cl-block nil
|
||||||
|
(dolist (g groups)
|
||||||
|
(dolist (b (ht-get g "books"))
|
||||||
|
(when (string= (ht-get b "title") "Edit Test Book")
|
||||||
|
(cl-return b)))))))
|
||||||
|
(let ((bm (find-book-model groups id)))
|
||||||
|
(should bm)
|
||||||
|
(let ((mb (ht-get bm "minibuffer")))
|
||||||
|
(should mb)
|
||||||
|
(let ((actions (ht-get mb "actions")))
|
||||||
|
(should (= (length actions) 2))
|
||||||
|
(should (string= (ht-get (nth 0 actions) "label") "Mark reading"))
|
||||||
|
(should (string= (ht-get (nth 1 actions) "label") "Add note"))))))))
|
||||||
|
(spine-edit-test--cleanup)))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-edit-test.el \
|
||||||
|
-l test/spine-index-model-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all pass.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add spine.el test/spine-edit-test.el
|
||||||
|
git commit -m "feat: add minibuffer actions to spine-index-model for selected books"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Add httpd/edit handler
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `spine.el` — add handler after `httpd/add`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add httpd/edit handler**
|
||||||
|
|
||||||
|
Insert after `httpd/add` (after line 369):
|
||||||
|
|
||||||
|
```elisp
|
||||||
|
(defun httpd/edit (proc uri-path query request)
|
||||||
|
"Handle /edit: GET shows prompt page, POST processes actions.
|
||||||
|
Query params:
|
||||||
|
id - book ID
|
||||||
|
action - status | note | rating
|
||||||
|
value - target status / rating value (for status/rating actions)
|
||||||
|
text - note text (for note action)
|
||||||
|
date - optional date string (for note/read actions)"
|
||||||
|
(let ((method (caar request))
|
||||||
|
(id (cadr (assoc "id" query)))
|
||||||
|
(action (cadr (assoc "action" query)))
|
||||||
|
(value (cadr (assoc "value" query)))
|
||||||
|
(text (cadr (assoc "text" query)))
|
||||||
|
(date (cadr (assoc "date" query))))
|
||||||
|
(if (equal method "POST")
|
||||||
|
(progn
|
||||||
|
(cond
|
||||||
|
((equal action "note")
|
||||||
|
(spine-add-note id text date))
|
||||||
|
((equal action "status")
|
||||||
|
(spine-set-status id value))
|
||||||
|
((equal action "rating")
|
||||||
|
(spine-set-rating id value)))
|
||||||
|
(httpd-redirect proc (format "/index?id=%s" id) 303))
|
||||||
|
;; GET: show prompt page for actions needing input
|
||||||
|
(if (and id (equal action "note"))
|
||||||
|
;; Show a simple note-prompt page
|
||||||
|
(let* ((books (spine-books))
|
||||||
|
(book (cl-find id books :key (lambda (b) (plist-get b :id)) :test #'equal))
|
||||||
|
(title (if book (plist-get book :title) "Unknown")))
|
||||||
|
(httpd-with-buffer proc "text/html"
|
||||||
|
(insert (format
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html lang=\"en\">
|
||||||
|
<head><meta charset=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>
|
||||||
|
<meta name=\"color-scheme\" content=\"light dark\"/>
|
||||||
|
<title>Spine — note</title>
|
||||||
|
<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css\"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class=\"container\">
|
||||||
|
<article style=\"max-width:560px;margin-inline:auto;\">
|
||||||
|
<header><strong>spine · add note</strong></header>
|
||||||
|
<p><em>%s</em></p>
|
||||||
|
<form method=\"post\" action=\"/edit\">
|
||||||
|
<input type=\"hidden\" name=\"id\" value=\"%s\"/>
|
||||||
|
<input type=\"hidden\" name=\"action\" value=\"note\"/>
|
||||||
|
<label>Date <input type=\"date\" name=\"date\" value=\"%s\"/></label>
|
||||||
|
<label>Note <textarea name=\"text\" rows=\"3\" required autofocus></textarea></label>
|
||||||
|
<button type=\"submit\">Add note</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>"
|
||||||
|
title id (format-time-string "%Y-%m-%d"))))))
|
||||||
|
;; Unknown action or missing id: redirect to index
|
||||||
|
(httpd-redirect proc "/index" 302)))))
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test the handler via curl (server test)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
SPINE_ORG_FILE=$(pwd)/sample-books.org emacs --batch -l spine.el \
|
||||||
|
--eval "(setq spine-org-file \"$(pwd)/sample-books.org\")" \
|
||||||
|
--eval "(httpd-start)" 2>&1 &
|
||||||
|
sleep 2
|
||||||
|
# Test note prompt page renders
|
||||||
|
curl -s "http://localhost:8080/edit?id=8c1e-uow&action=note" 2>/dev/null | grep -oP "add note|Use of Weapons|textarea" | sort | uniq -c
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: shows "add note", "Use of Weapons", "textarea".
|
||||||
|
|
||||||
|
- [ ] **Step 3: Kill the server**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkill -f "emacs.*spine.el" 2>/dev/null; sleep 1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add spine.el
|
||||||
|
git commit -m "feat: add /edit handler with note prompt page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Update template minibuffer section
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `templates/index.mustache`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace minibuffer placeholder with action links**
|
||||||
|
|
||||||
|
Find the existing minibuffer section (lines ~127-133) and replace with:
|
||||||
|
|
||||||
|
```mustache
|
||||||
|
{{#minibuffer}}
|
||||||
|
<footer class="minibuffer">
|
||||||
|
<span class="muted">M-x</span>
|
||||||
|
<span style="display:flex;gap:.5rem;flex-wrap:wrap">
|
||||||
|
{{#actions}}
|
||||||
|
<a href="{{href}}" class="pill" style="text-decoration:none">{{label}}</a>
|
||||||
|
{{/actions}}
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
{{/minibuffer}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove any empty minibuffer template section if it existed (the current template has a minibuffer section with `command`/`text` fields that should be replaced).
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify template renders correctly**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch --eval '(setq spine-skip-server-start t)' \
|
||||||
|
--eval '(setq spine-org-file "/work/personal-local/spine/sample-books.org")' \
|
||||||
|
-l spine.el \
|
||||||
|
--eval '(let* ((books (spine-books)) (model (spine-index-model books nil "8c1e-uow")) (html (spine-render "index.mustache" model))) (with-temp-file "/tmp/spine-edit-test.html" (insert html)))' 2>&1
|
||||||
|
grep -oP "M-x|Mark read|Abandon|Add note" /tmp/spine-edit-test.html | sort | uniq -c
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: shows "M-x", "Mark read", "Abandon", "Add note" (for a READING book).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add templates/index.mustache
|
||||||
|
git commit -m "feat: update minibuffer to render context-sensitive action links"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Final integration and test run
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run all tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /work/personal-local/spine
|
||||||
|
emacs --batch -l test/spine-edit-test.el \
|
||||||
|
-l test/spine-index-model-test.el \
|
||||||
|
-l test/spine-books-test.el \
|
||||||
|
-l test/spine-add-book-test.el \
|
||||||
|
--eval "(ert-run-tests-batch-and-exit '(not (tag :unstable)))" 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Push to main**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user