This is an automated email from the ASF dual-hosted git repository.

gstein pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/steve.git

commit e02bb65af88fb6e4b0eff915277e769c7ded09a6
Author: Greg Stein <[email protected]>
AuthorDate: Fri Oct 17 00:53:03 2025 -0500

    Add Issue management for elections.
    
    * manage.ezt:
      - "Add Issue" button
      - "Expand/Collapse All" descriptions of existing issues
      - model for add/edit an issue.
      - each issue gets a twiddle for the description, and icons to edit
        or delete the issue.
      - initial JS code for issue management
    - pages.py:
      - .get_issue() returns a tuple (fix!); turn into edict
      - endpoints that mutate should use 303 (See Other) for their
        redirect, rather than telling the browser to retry $method at a
        new Location (eg. do a GET at new loc, not DELETE at new loc)
    * steve.css: small tweak to support issue management buttons
---
 v3/server/pages.py             |  15 ++--
 v3/server/static/css/steve.css |   1 +
 v3/server/templates/manage.ezt | 172 +++++++++++++++++++++++++++++++++++++++--
 3 files changed, 176 insertions(+), 12 deletions(-)

diff --git a/v3/server/pages.py b/v3/server/pages.py
index 912e965..557101b 100644
--- a/v3/server/pages.py
+++ b/v3/server/pages.py
@@ -176,7 +176,10 @@ def load_election_issue(func):
             raise_404(T_BAD_IID, result)
             # NOTREACHED
 
-        return await func(e, i)
+        ### get_issue() should return an edict. fix it here.
+        issue = edict(iid=iid, title=i[0], description=i[1], type=i[2], 
kv=i[3])
+
+        return await func(e, issue)
 
     return loader
 
@@ -283,7 +286,7 @@ async def do_open_endpoint(election):
     await flash_success(f'Opened election: {title}')
 
     # Return to the management page for this Election.
-    return quart.redirect(f'/manage/{election.eid}')
+    return quart.redirect(f'/manage/{election.eid}', code=303)
 
 
 @APP.get('/do-close/<eid>')
@@ -303,7 +306,7 @@ async def do_close_endpoint(election):
     await flash_success(f'Closed election: {title}')
 
     # Return to the management page for this Election.
-    return quart.redirect(f'/manage/{election.eid}')
+    return quart.redirect(f'/manage/{election.eid}', code=303)
 
 
 @APP.post('/do-add-issue/<eid>')
@@ -320,7 +323,7 @@ async def do_add_issue_endpoint(election):
                  f' to election[E:{election.eid}]')
 
     # Return to the management page for this Election.
-    return quart.redirect(f'/manage/{election.eid}')
+    return quart.redirect(f'/manage/{election.eid}', code=303)
 
 
 @APP.post('/do-edit-issue/<eid>/<iid>')
@@ -337,7 +340,7 @@ async def do_edit_issue_endpoint(election, issue):
                  f' in election[E:{election.eid}]')
 
     # Return to the management page for this Election.
-    return quart.redirect(f'/manage/{election.eid}')
+    return quart.redirect(f'/manage/{election.eid}', code=303)
 
 
 @APP.delete('/do-delete-issue/<eid>/<iid>')
@@ -354,7 +357,7 @@ async def do_delete_issue_endpoint(election, issue):
                  f' from election[E:{election.eid}]')
 
     # Return to the management page for this Election.
-    return quart.redirect(f'/manage/{election.eid}')
+    return quart.redirect(f'/manage/{election.eid}', code=303)
 
 
 @APP.get('/profile')
diff --git a/v3/server/static/css/steve.css b/v3/server/static/css/steve.css
index 6118779..f29aedb 100644
--- a/v3/server/static/css/steve.css
+++ b/v3/server/static/css/steve.css
@@ -52,6 +52,7 @@
 
 /* vote-on.ezt  */
 .issue-item { margin-bottom: 1rem; }
+.action-buttons { margin-left: 1rem; }
 .vote-radio { margin-left: 1rem; }
 .description { display: none; }
 .description.show { display: block; }
diff --git a/v3/server/templates/manage.ezt b/v3/server/templates/manage.ezt
index 956c437..de0768f 100644
--- a/v3/server/templates/manage.ezt
+++ b/v3/server/templates/manage.ezt
@@ -42,7 +42,7 @@
                 </div>
             </div>
         </div>
-    </div>
+    </div>[# id=openConfirmModal ]
 
     <!-- Close Confirmation Modal -->
     <div class="modal fade" id="closeConfirmModal" tabindex="-1" 
aria-labelledby="closeConfirmModalLabel" aria-hidden="true">
@@ -61,8 +61,37 @@
                 </div>
             </div>
         </div>
-    </div>
+    </div>[# id=closeConfirmModal ]
 
+    <!-- Edit/Add Issue Modal -->
+    <div class="modal fade" id="issueModal" tabindex="-1" 
aria-labelledby="issueModalLabel" aria-hidden="true">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title" id="issueModalLabel">Edit 
Issue</h5>
+                    <button type="button" class="btn-close" 
data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    <form id="issueForm">
+                        <input type="hidden" id="issueId" value="">
+                        <div class="mb-3">
+                            <label for="issueTitle" 
class="form-label">Title</label>
+                            <input type="text" class="form-control" 
id="issueTitle" required>
+                            <div class="invalid-feedback">Title is 
required.</div>
+                        </div>
+                        <div class="mb-3">
+                            <label for="issueDescription" 
class="form-label">Description</label>
+                            <textarea class="form-control" 
id="issueDescription" rows="4"></textarea>
+                        </div>
+                    </form>
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" 
data-bs-dismiss="modal">Cancel</button>
+                    <button type="button" class="btn btn-primary" 
onclick="saveIssue()">Save</button>
+                </div>
+            </div>[# modal-content ]
+        </div>[# modal-dialog ]
+    </div>[# id=issueModal ]
 
         <div>
             State: [e_state]
@@ -73,11 +102,41 @@
         <div class="mb-3">
             Estimate to close: <input type="date">
         </div>
-        <ul>
+
+        <div class="mb-4">
+            <button type="button" class="btn btn-primary" 
onclick="openAddIssueModal()">Add Issue</button>
+            <button type="button" class="btn btn-outline-secondary ms-2" 
onclick="toggleAllDescriptions()">
+                <span id="toggle-all-text">Expand All</span>
+            </button>
+        </div>
+
+        <div id="issues-list" class="list-group">
             [for issues]
-                <li>[issues.title]</li>
-            [end]
-        </ul>
+            <div class="list-group-item issue-item">
+                <div class="d-flex justify-content-between align-items-center">
+                    <div>
+                        <span class="twiddle bi bi-caret-right-fill me-2" 
onclick="toggleDescription('[issues.iid]')"></span>
+                        <strong>[issues.title]</strong>
+                    </div>
+                    <div class="action-buttons">
+                        <button type="button" class="btn btn-outline-primary 
btn-sm me-1"
+                              onclick="openEditIssueModal('[issues.iid]',
+                                                          '[format 
"js,html"][issues.title][end]',
+                                                          '[format 
"js,html"][issues.description][end]')"
+                              aria-label="Edit Issue">
+                            <i class="bi bi-pencil"></i>
+                        </button>
+                        <button type="button" class="btn btn-outline-danger 
btn-sm"
+                              onclick="deleteIssue('[issues.iid]')" 
aria-label="Delete Issue">
+                            <i class="bi bi-trash"></i>
+                        </button>
+                    </div>
+               </div>
+               <div id="description-[issues.iid]" class="description 
mt-2">[issues.description]</div>
+           </div>
+           [end]
+       </div>
+
     </div>
 
 [include "footer.ezt"]
@@ -109,4 +168,105 @@
             closeModal.hide();
             window.location.href = '/do-close/[eid]';
         });
+
+    // Toggle individual description
+    function toggleDescription(issueId) {
+      const desc = document.getElementById(`description-${issueId}`);
+      const twiddle = desc.previousElementSibling.querySelector('.twiddle');
+      if (desc && twiddle) {
+        desc.classList.toggle('show');
+        twiddle.classList.toggle('bi-caret-right-fill');
+        twiddle.classList.toggle('bi-caret-down-fill');
+      }
+    }
+
+
+    // Expand/Collapse All descriptions
+    let allExpanded = false;
+    function toggleAllDescriptions() {
+      allExpanded = !allExpanded;
+      document.querySelectorAll('.description').forEach(desc => {
+        desc.classList.toggle('show', allExpanded);
+      });
+      document.querySelectorAll('.twiddle').forEach(twiddle => {
+        twiddle.classList.toggle('bi-caret-right-fill', !allExpanded);
+        twiddle.classList.toggle('bi-caret-down-fill', allExpanded);
+      });
+      document.getElementById('toggle-all-text').textContent = allExpanded ? 
'Collapse All' : 'Expand All';
+    }
+
+    // Open modal for adding issue
+    function openAddIssueModal() {
+      document.getElementById('issueModalLabel').textContent = 'Add Issue';
+      document.getElementById('issueId').value = '';
+      document.getElementById('issueTitle').value = '';
+      document.getElementById('issueDescription').value = '';
+      document.getElementById('issueTitle').classList.remove('is-invalid');
+      const modal = new bootstrap.Modal(document.getElementById('issueModal'));
+      modal.show();
+    }
+
+    // Open modal for editing issue
+    function openEditIssueModal(issueId, title, description) {
+      document.getElementById('issueModalLabel').textContent = 'Edit Issue';
+      document.getElementById('issueId').value = issueId;
+      document.getElementById('issueTitle').value = title;
+      document.getElementById('issueDescription').value = description;
+      document.getElementById('issueTitle').classList.remove('is-invalid');
+      const modal = new bootstrap.Modal(document.getElementById('issueModal'));
+      modal.show();
+    }
+
+    // Save issue (add or edit)
+    function saveIssue() {
+      const issueId = document.getElementById('issueId').value;
+      const title = document.getElementById('issueTitle').value.trim();
+      const description = 
document.getElementById('issueDescription').value.trim();
+      const titleInput = document.getElementById('issueTitle');
+
+      // Client-side validation
+      if (!title) {
+        titleInput.classList.add('is-invalid');
+        return;
+      }
+      titleInput.classList.remove('is-invalid');
+
+      const url = issueId ? `/do-edit-issue/[eid]/${issueId}` : 
`/do-add-issue/[eid]`;
+      fetch(url, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ title, description }),
+      })
+        .then(response => {
+          if (!response.ok) throw new Error('Network response was not ok');
+          return response.json();
+        })
+        .then(data => {
+          
bootstrap.Modal.getInstance(document.getElementById('issueModal')).hide();
+          // Server will refresh page with flash messages
+        })
+        .catch(error => {
+          // Server will handle error and flash message
+        });
+    }
+
+    // Delete issue
+    function deleteIssue(issueId) {
+      if (!confirm('Are you sure you want to delete this issue?')) return;
+
+      fetch(`/do-delete-issue/[eid]/${issueId}`, {
+        method: 'DELETE',
+      })
+        .then(response => {
+          if (!response.ok) throw new Error('Network response was not ok');
+          return response.json();
+        })
+        .then(data => {
+          // Server will refresh page with flash messages
+        })
+        .catch(error => {
+          // Server will handle error and flash message
+        });
+    }
+
 </script>

Reply via email to