feat(workspace): stack hashtag back button above heading and add dedicated hashtag markdown note (#tag.md)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
f1d9daca60
commit
b0403e09bc
6 files changed
+379
-166
No files matched your search
@@ -106,6 +106,7 @@ func (s *Server) Router() http.Handler {
|
||||
// Workspace Tags & Pages
|
||||
r.Get("/api/v1/workspace/tags", s.handleGetWorkspaceTags)
|
||||
r.Get("/api/v1/workspace/tags/{tag}", s.handleGetTagDetails)
|
||||
r.Put("/api/v1/workspace/tags/{tag}/note", s.handleUpdateTagPageNote)
|
||||
|
||||
r.Get("/api/v1/documents", s.handleGetDocuments)
|
||||
r.Post("/api/v1/documents", s.handleCreateDocument)
|
||||
@@ -903,6 +904,28 @@ func (s *Server) handleGetTagDetails(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, details)
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateTagPageNote(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
tag := chi.URLParam(r, "tag")
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
updatedAt, err := s.store.UpdateTagPageNote(userID, tag, req.Content)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"tag": tag,
|
||||
"content": req.Content,
|
||||
"updatedAt": updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// --- DOCUMENT SHARING & COLLABORATION ---
|
||||
|
||||
func (s *Server) handleGetDocShares(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -314,9 +314,11 @@ type TagDocumentGroup struct {
|
||||
}
|
||||
|
||||
type TagDetails struct {
|
||||
Tag string `json:"tag"`
|
||||
Documents []TagDocumentGroup `json:"documents"`
|
||||
TotalMatches int `json:"totalMatches"`
|
||||
Tag string `json:"tag"`
|
||||
NoteContent string `json:"noteContent"`
|
||||
NoteUpdatedAt string `json:"noteUpdatedAt,omitempty"`
|
||||
Documents []TagDocumentGroup `json:"documents"`
|
||||
TotalMatches int `json:"totalMatches"`
|
||||
}
|
||||
|
||||
type SpaceMember struct {
|
||||
|
||||
@@ -249,6 +249,14 @@ func (s *Store) migrate() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_block_tags_user_tag ON doc_block_tags(user_id, tag);
|
||||
CREATE INDEX IF NOT EXISTS idx_doc_block_tags_doc ON doc_block_tags(doc_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tag_pages (
|
||||
user_id TEXT NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY(user_id, tag)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lists (
|
||||
id TEXT PRIMARY KEY,
|
||||
space_id TEXT NOT NULL,
|
||||
@@ -1577,6 +1585,41 @@ func (s *Store) indexDocBlockTags(docID, userID string, blocks []domain.Document
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) saveTagPageToDisk(userID, tag, content string) {
|
||||
if s.docDir == "" || tag == "" {
|
||||
return
|
||||
}
|
||||
tagDir := filepath.Join(filepath.Dir(s.docDir), "tag_pages", userID)
|
||||
_ = os.MkdirAll(tagDir, 0755)
|
||||
safeTag := regexp.MustCompile(`[^a-zA-Z0-9äöüÄÖÜß_-]+`).ReplaceAllString(tag, "_")
|
||||
targetPath := filepath.Join(tagDir, safeTag+".md")
|
||||
if strings.TrimSpace(content) == "" {
|
||||
_ = os.Remove(targetPath)
|
||||
return
|
||||
}
|
||||
md := fmt.Sprintf("# #%s\n\n%s\n", tag, content)
|
||||
_ = os.WriteFile(targetPath, []byte(md), 0644)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateTagPageNote(userID, tag, content string) (string, error) {
|
||||
cleanTag := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(tag), "#"))
|
||||
if cleanTag == "" {
|
||||
return "", errors.New("tag name required")
|
||||
}
|
||||
now := domain.NowISO()
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO tag_pages (user_id, tag, content, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, tag) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at`,
|
||||
userID, cleanTag, content, now,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.saveTagPageToDisk(userID, cleanTag, content)
|
||||
return now, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetWorkspaceTags(userID string) ([]domain.TagSummary, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT t.tag, COUNT(DISTINCT t.doc_id || ':' || t.block_id) as count
|
||||
@@ -1595,12 +1638,32 @@ func (s *Store) GetWorkspaceTags(userID string) ([]domain.TagSummary, error) {
|
||||
defer rows.Close()
|
||||
|
||||
results := make([]domain.TagSummary, 0)
|
||||
seen := make(map[string]bool)
|
||||
for rows.Next() {
|
||||
var ts domain.TagSummary
|
||||
if err := rows.Scan(&ts.Tag, &ts.Count); err == nil {
|
||||
results = append(results, ts)
|
||||
seen[ts.Tag] = true
|
||||
}
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
// Also include tags that have a non-empty tag_pages note even if not currently used in a block
|
||||
pageRows, err := s.db.Query(
|
||||
`SELECT tag FROM tag_pages WHERE user_id = ? AND TRIM(content) != '' ORDER BY tag ASC`,
|
||||
userID,
|
||||
)
|
||||
if err == nil {
|
||||
for pageRows.Next() {
|
||||
var t string
|
||||
if err := pageRows.Scan(&t); err == nil && t != "" && !seen[t] {
|
||||
results = append(results, domain.TagSummary{Tag: t, Count: 0})
|
||||
seen[t] = true
|
||||
}
|
||||
}
|
||||
_ = pageRows.Close()
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -1610,6 +1673,12 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
|
||||
return &domain.TagDetails{Tag: "", Documents: []domain.TagDocumentGroup{}, TotalMatches: 0}, nil
|
||||
}
|
||||
|
||||
var noteContent, noteUpdatedAt string
|
||||
_ = s.db.QueryRow(
|
||||
`SELECT content, updated_at FROM tag_pages WHERE user_id = ? AND tag = ?`,
|
||||
userID, cleanTag,
|
||||
).Scan(¬eContent, ¬eUpdatedAt)
|
||||
|
||||
rows, err := s.db.Query(
|
||||
`SELECT t.block_id, d.id, d.title, d.icon, d.folder_path, d.blocks_json, d.updated_at, d.user_id,
|
||||
COALESCE(u.display_name, '') as owner_name,
|
||||
@@ -1734,9 +1803,11 @@ func (s *Store) GetTagDetails(userID, tag string) (*domain.TagDetails, error) {
|
||||
}
|
||||
|
||||
return &domain.TagDetails{
|
||||
Tag: cleanTag,
|
||||
Documents: docGroups,
|
||||
TotalMatches: totalMatches,
|
||||
Tag: cleanTag,
|
||||
NoteContent: noteContent,
|
||||
NoteUpdatedAt: noteUpdatedAt,
|
||||
Documents: docGroups,
|
||||
TotalMatches: totalMatches,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user