feat: create request list manually; larger action buttons; Summary View/Detailed View toggle
This commit is contained in:
parent
2f25eceac7
commit
ce2c68634d
|
|
@ -1933,6 +1933,63 @@ func (h *Handlers) ListRequests(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// ImportRequests handles POST /api/projects/{projectID}/requests/import
|
// ImportRequests handles POST /api/projects/{projectID}/requests/import
|
||||||
// Accepts multipart form with CSV/XLSX file, mode (add/replace), section_filter, create_workstreams
|
// 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) {
|
func (h *Handlers) ImportRequests(w http.ResponseWriter, r *http.Request) {
|
||||||
actorID := UserIDFromContext(r.Context())
|
actorID := UserIDFromContext(r.Context())
|
||||||
projectID := chi.URLParam(r, "projectID")
|
projectID := chi.URLParam(r, "projectID")
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
|
||||||
// Requests (list, tree, and import)
|
// Requests (list, tree, and import)
|
||||||
r.Get("/projects/{projectID}/requests", h.ListRequests)
|
r.Get("/projects/{projectID}/requests", h.ListRequests)
|
||||||
r.Get("/projects/{projectID}/requests/tree", h.ListRequestTree)
|
r.Get("/projects/{projectID}/requests/tree", h.ListRequestTree)
|
||||||
|
r.Post("/projects/{projectID}/requests", h.CreateRequestList)
|
||||||
r.Post("/projects/{projectID}/requests/import", h.ImportRequests)
|
r.Post("/projects/{projectID}/requests/import", h.ImportRequests)
|
||||||
|
|
||||||
// Request detail
|
// Request detail
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,9 @@
|
||||||
<button class="tab pb-3 text-base font-medium text-[#94a3b8] transition" onclick="switchTab('orgs', this)">Parties</button>
|
<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">
|
<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>
|
<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 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 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="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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -71,6 +72,24 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- 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 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">
|
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
|
||||||
|
|
@ -309,7 +328,7 @@
|
||||||
// ---- Detail toggle ----
|
// ---- Detail toggle ----
|
||||||
function toggleDetailMode() {
|
function toggleDetailMode() {
|
||||||
detailMode = !detailMode;
|
detailMode = !detailMode;
|
||||||
document.getElementById('detailToggle').textContent = detailMode ? 'Full' : 'Summary';
|
document.getElementById('detailToggle').textContent = detailMode ? 'Detailed View' : 'Summary View';
|
||||||
renderTree();
|
renderTree();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1176,6 +1195,35 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Import ----
|
// ---- 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() {
|
async function openImportModal() {
|
||||||
document.getElementById('importModal').style.display='flex';
|
document.getElementById('importModal').style.display='flex';
|
||||||
document.getElementById('importFile').value='';
|
document.getElementById('importFile').value='';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue