feat: create request list manually; larger action buttons; Summary View/Detailed View toggle

This commit is contained in:
James 2026-03-14 18:42:21 -04:00
parent 2f25eceac7
commit ce2c68634d
4 changed files with 109 additions and 3 deletions

View File

@ -1933,6 +1933,63 @@ func (h *Handlers) ListRequests(w http.ResponseWriter, r *http.Request) {
// ImportRequests handles POST /api/projects/{projectID}/requests/import
// Accepts multipart form with CSV/XLSX file, mode (add/replace), section_filter, create_workstreams
// CreateRequestList creates a new empty request list for a project.
func (h *Handlers) CreateRequestList(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
projectID := chi.URLParam(r, "projectID")
if err := lib.CheckAccessWrite(h.DB, actorID, projectID, ""); err != nil {
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
return
}
var body struct {
Name string `json:"name"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || strings.TrimSpace(body.Name) == "" {
ErrorResponse(w, http.StatusBadRequest, "invalid_body", "name is required")
return
}
projectKey, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projectID)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Could not derive project key")
return
}
now := time.Now().UTC().Format(time.RFC3339)
listName := strings.TrimSpace(body.Name)
requestListID := uuid.New().String()
rlData := lib.RequestListData{Name: listName}
rlDataJSON, _ := json.Marshal(rlData)
rlSummaryPacked, _ := lib.Pack(projectKey, listName)
rlDataPacked, _ := lib.Pack(projectKey, string(rlDataJSON))
_, err = h.DB.Conn.Exec(
`INSERT INTO entries (entry_id, project_id, parent_id, type, depth, sort_order,
search_key, search_key2, summary, data, stage,
assignee_id, return_to_id, origin_id,
version, deleted_at, deleted_by, key_version,
created_at, updated_at, created_by)
VALUES (?,?,?,?,?,?, ?,?,?,?,?, ?,?,?, ?,?,?,?, ?,?,?)`,
requestListID, projectID, projectID, lib.TypeRequestList, 1, 0,
nil, nil, rlSummaryPacked, rlDataPacked, lib.StagePreDataroom,
"", "", "",
1, nil, nil, 1,
now, now, actorID,
)
if err != nil {
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to create request list")
return
}
JSONResponse(w, http.StatusOK, map[string]any{
"entry_id": requestListID,
"name": listName,
})
}
func (h *Handlers) ImportRequests(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
projectID := chi.URLParam(r, "projectID")

View File

@ -70,6 +70,7 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
// Requests (list, tree, and import)
r.Get("/projects/{projectID}/requests", h.ListRequests)
r.Get("/projects/{projectID}/requests/tree", h.ListRequestTree)
r.Post("/projects/{projectID}/requests", h.CreateRequestList)
r.Post("/projects/{projectID}/requests/import", h.ImportRequests)
// Request detail

BIN
dealspace

Binary file not shown.

View File

@ -23,8 +23,9 @@
<button class="tab pb-3 text-base font-medium text-[#94a3b8] transition" onclick="switchTab('orgs', this)">Parties</button>
<div class="ml-auto flex items-center gap-2 pb-2">
<span id="projectStatus" class="px-2 py-0.5 rounded text-xs font-medium"></span>
<button onclick="openImportModal()" class="px-3 py-1 bg-white/[0.07] hover:bg-white/[0.12] text-white rounded text-xs font-medium transition">Import</button>
<button id="detailToggle" onclick="toggleDetailMode()" class="px-3 py-1 bg-white/[0.07] hover:bg-white/[0.12] text-[#cbd5e1] hover:text-white rounded text-xs font-medium transition">Summary</button>
<button onclick="openCreateListModal()" class="px-3.5 py-1.5 bg-white/[0.07] hover:bg-white/[0.12] text-white rounded text-sm font-medium transition">+ New List</button>
<button onclick="openImportModal()" class="px-3.5 py-1.5 bg-white/[0.07] hover:bg-white/[0.12] text-white rounded text-sm font-medium transition">Import</button>
<button id="detailToggle" onclick="toggleDetailMode()" class="px-3.5 py-1.5 bg-white/[0.07] hover:bg-white/[0.12] text-[#cbd5e1] hover:text-white rounded text-sm font-medium transition">Summary View</button>
</div>
</div>
@ -71,6 +72,24 @@
</div>
</div>
<!-- Create List Modal -->
<div id="createListModal" style="display:none" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-[#0d1f3c] rounded-xl p-6 w-full max-w-md border border-white/[0.08]">
<h2 class="text-lg font-semibold text-white mb-4">New Request List</h2>
<div id="createListError" class="hidden mb-3 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
<div class="mb-4">
<label class="block text-sm font-medium text-[#94a3b8] mb-1.5">List name</label>
<input type="text" id="createListName" placeholder="e.g. Legal Due Diligence, Financial Review"
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] text-sm"
onkeydown="if(event.key==='Enter')submitCreateList()">
</div>
<div class="flex gap-3">
<button onclick="closeCreateListModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
<button id="createListBtn" onclick="submitCreateList()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Create</button>
</div>
</div>
</div>
<!-- Import Modal -->
<div id="importModal" style="display:none" class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
@ -309,7 +328,7 @@
// ---- Detail toggle ----
function toggleDetailMode() {
detailMode = !detailMode;
document.getElementById('detailToggle').textContent = detailMode ? 'Full' : 'Summary';
document.getElementById('detailToggle').textContent = detailMode ? 'Detailed View' : 'Summary View';
renderTree();
}
@ -1176,6 +1195,35 @@
}
// ---- Import ----
function openCreateListModal() {
document.getElementById('createListName').value = '';
document.getElementById('createListError').classList.add('hidden');
document.getElementById('createListModal').style.display = 'flex';
setTimeout(() => document.getElementById('createListName').focus(), 50);
}
function closeCreateListModal() { document.getElementById('createListModal').style.display = 'none'; }
async function submitCreateList() {
const name = document.getElementById('createListName').value.trim();
if (!name) return;
const btn = document.getElementById('createListBtn');
btn.disabled = true; btn.textContent = 'Creating...';
try {
const res = await fetchAPI('/api/projects/' + projectID + '/requests', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name})
});
if (!res.ok) { const e = await res.json(); throw new Error(e.message || 'Failed'); }
closeCreateListModal();
await loadRequestTree();
} catch(e) {
document.getElementById('createListError').textContent = e.message;
document.getElementById('createListError').classList.remove('hidden');
} finally {
btn.disabled = false; btn.textContent = 'Create';
}
}
async function openImportModal() {
document.getElementById('importModal').style.display='flex';
document.getElementById('importFile').value='';