feat: document comments
Add file_comments table and FileComment model. Add comment icon per file that toggles inline comment panel. Comments loaded via HTMX with add form, stored per file/deal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9295b18560
commit
2d6e9fc79a
|
|
@ -21,6 +21,7 @@ func Migrate(db *sql.DB) error {
|
|||
createInvites,
|
||||
createBuyerGroups,
|
||||
createFolderAccess,
|
||||
createFileComments,
|
||||
}
|
||||
|
||||
for i, m := range migrations {
|
||||
|
|
@ -219,6 +220,16 @@ CREATE TABLE IF NOT EXISTS folder_access (
|
|||
PRIMARY KEY (folder_id, buyer_group)
|
||||
);`
|
||||
|
||||
const createFileComments = `
|
||||
CREATE TABLE IF NOT EXISTS file_comments (
|
||||
id TEXT PRIMARY KEY,
|
||||
file_id TEXT NOT NULL,
|
||||
deal_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
// Additive migrations - each statement is run individually, errors ignored (for already-existing columns)
|
||||
var additiveMigrationStmts = []string{
|
||||
// Section 1: org_type
|
||||
|
|
|
|||
|
|
@ -158,6 +158,67 @@ func (h *Handler) logActivity(dealID, userID, orgID, actType, resourceType, reso
|
|||
id, orgID, dealID, userID, actType, resourceType, resourceName, resourceID)
|
||||
}
|
||||
|
||||
func (h *Handler) handleFileComments(w http.ResponseWriter, r *http.Request) {
|
||||
profile := getProfile(r.Context())
|
||||
// Parse: /deals/files/comments/{fileID}
|
||||
fileID := strings.TrimPrefix(r.URL.Path, "/deals/files/comments/")
|
||||
|
||||
// Get deal_id for this file
|
||||
var dealID string
|
||||
h.db.QueryRow("SELECT deal_id FROM files WHERE id = ?", fileID).Scan(&dealID)
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
content := strings.TrimSpace(r.FormValue("content"))
|
||||
if content != "" {
|
||||
id := generateID("cmt")
|
||||
h.db.Exec("INSERT INTO file_comments (id, file_id, deal_id, user_id, content) VALUES (?, ?, ?, ?, ?)",
|
||||
id, fileID, dealID, profile.ID, content)
|
||||
}
|
||||
}
|
||||
|
||||
// Get comments
|
||||
rows, err := h.db.Query(`
|
||||
SELECT c.id, c.content, c.created_at, COALESCE(p.full_name, 'Unknown')
|
||||
FROM file_comments c LEFT JOIN profiles p ON c.user_id = p.id
|
||||
WHERE c.file_id = ? ORDER BY c.created_at ASC`, fileID)
|
||||
if err != nil {
|
||||
http.Error(w, "Error loading comments", 500)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
// Build HTML response
|
||||
html := `<div class="space-y-3 max-h-64 overflow-y-auto mb-4">`
|
||||
hasComments := false
|
||||
for rows.Next() {
|
||||
hasComments = true
|
||||
var id, content, userName string
|
||||
var createdAt string
|
||||
rows.Scan(&id, &content, &createdAt, &userName)
|
||||
html += fmt.Sprintf(`<div class="bg-gray-800 rounded-lg p-3">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-medium text-teal-400">%s</span>
|
||||
<span class="text-xs text-gray-600">%s</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-300">%s</p>
|
||||
</div>`, userName, createdAt, content)
|
||||
}
|
||||
if !hasComments {
|
||||
html += `<p class="text-sm text-gray-500 italic">No comments yet.</p>`
|
||||
}
|
||||
html += `</div>`
|
||||
|
||||
// Add comment form
|
||||
html += fmt.Sprintf(`<form hx-post="/deals/files/comments/%s" hx-target="#comments-%s" hx-swap="innerHTML" class="flex gap-2">
|
||||
<input type="text" name="content" placeholder="Add a comment..." class="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
||||
<button type="submit" class="px-3 py-2 bg-teal-500 text-white text-sm rounded-lg hover:bg-teal-600 transition">Send</button>
|
||||
</form>`, fileID, fileID)
|
||||
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ mux.HandleFunc("/auth/logout", h.handleLogout)
|
|||
mux.HandleFunc("/deals/files/upload", h.requireAuth(h.handleFileUpload))
|
||||
mux.HandleFunc("/deals/files/delete", h.requireAuth(h.handleFileDelete))
|
||||
mux.HandleFunc("/deals/files/download/", h.requireAuth(h.handleFileDownload))
|
||||
mux.HandleFunc("/deals/files/comments/", h.requireAuth(h.handleFileComments))
|
||||
|
||||
// HTMX partials
|
||||
mux.HandleFunc("/htmx/request-comment", h.requireAuth(h.handleUpdateComment))
|
||||
|
|
|
|||
|
|
@ -144,6 +144,16 @@ type Session struct {
|
|||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type FileComment struct {
|
||||
ID string
|
||||
FileID string
|
||||
DealID string
|
||||
UserID string
|
||||
Content string
|
||||
CreatedAt time.Time
|
||||
UserName string // computed
|
||||
}
|
||||
|
||||
type Invite struct {
|
||||
Token string
|
||||
OrgID string
|
||||
|
|
|
|||
|
|
@ -152,6 +152,9 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.
|
|||
</td>
|
||||
<td class="px-4 py-2.5 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button hx-get={ fmt.Sprintf("/deals/files/comments/%s", file.ID) } hx-target={ fmt.Sprintf("#comments-%s", file.ID) } hx-swap="innerHTML" onclick={ templ.ComponentScript{Call: fmt.Sprintf("document.getElementById('commentPanel-%s').classList.toggle('hidden')", file.ID)} } class="text-xs text-gray-500 hover:text-teal-400 transition" title="Comments">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
|
||||
</button>
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/deals/files/download/%s", file.ID)) } class="text-xs text-gray-500 hover:text-teal-400 transition" title="Download">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
|
||||
</a>
|
||||
|
|
@ -167,6 +170,13 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id={ fmt.Sprintf("commentPanel-%s", file.ID) } class="hidden">
|
||||
<td colspan="6" class="px-4 py-3 bg-gray-800/30">
|
||||
<div id={ fmt.Sprintf("comments-%s", file.ID) }>
|
||||
<p class="text-xs text-gray-500 italic">Click the comment icon to load comments.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
|
|
|
|||
Loading…
Reference in New Issue