Files
MindSift/internal/api/restructure.go
T
sandrodachandClaude Opus 5.5 4d1b37c956
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
feat(workspace): turn a branch into its own note, merge notes
- Extract: button on the bullet (desktop) or /extract (/notiz). The bullet
  becomes the title (hashtags stay at the link), its children the content
  of a new note in the same folder with the source's shares (a foreign
  source owner gets write access); the branch becomes [[title]] through
  the operation log, so open editors follow.
- Merge: drop a note onto another in the sidebar or use "Mit anderer
  Notiz zusammenführen…" in the note header. The own source is appended
  as an h2 section with its bullets below and goes to the trash; the toast
  offers a complete undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:25:46 +01:00

124 lines
4.5 KiB
Go

package api
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/sandrodach/mindsift/internal/auth"
"github.com/sandrodach/mindsift/internal/collab"
"github.com/sandrodach/mindsift/internal/storage"
)
// Turning a branch into its own note and merging notes (storage/restructure.go). The editor sends its
// client ID, so its own block leases do not block the change.
func (s *Server) restructureActor(r *http.Request, clientID string) collab.Actor {
userID := auth.GetUserID(r.Context())
a := collab.Actor{UserID: userID, ClientID: clientID}
if u, err := s.store.GetUserByID(userID); err == nil {
a.Name = u.DisplayName
}
return a
}
func restructureStatus(err error) int {
switch {
case errors.Is(err, storage.ErrBranchLocked):
return http.StatusConflict
case errors.Is(err, storage.ErrBlockNotFound), errors.Is(err, storage.ErrDocNotFound):
return http.StatusNotFound
case errors.Is(err, storage.ErrMergeSelf), errors.Is(err, storage.ErrDocInTrash):
return http.StatusBadRequest
case errors.Is(err, storage.ErrMergeNotOwner):
return http.StatusForbidden
}
return http.StatusInternalServerError
}
// handleExtractBranch: POST /documents/{id}/extract {blockId, clientId}
func (s *Server) handleExtractBranch(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id")
if !storage.CanEditDoc(s.store.DocRole(userID, docID)) {
writeError(w, http.StatusForbidden, "Keine Schreibrechte für diese Notiz")
return
}
var req struct {
BlockID string `json:"blockId"`
ClientID string `json:"clientId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.BlockID == "" {
writeError(w, http.StatusBadRequest, "blockId fehlt")
return
}
created, err := s.store.ExtractBranch(userID, docID, req.BlockID, s.restructureActor(r, req.ClientID), s.leases.View(docID))
if err != nil {
writeError(w, restructureStatus(err), err.Error())
return
}
s.notifyDocChanged(docID, s.store.DocHead(docID))
if full, err := s.store.GetDocumentByID(userID, created.ID); err == nil {
created = full
}
s.broadcastDocToAuthorizedUsers(created.ID, "DOC_CREATED")
writeJSON(w, http.StatusCreated, created)
}
// handleMergeDocuments: POST /documents/{id}/merge {sourceId, clientId} appends the source to this note.
func (s *Server) handleMergeDocuments(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
targetID := chi.URLParam(r, "id")
if !storage.CanEditDoc(s.store.DocRole(userID, targetID)) {
writeError(w, http.StatusForbidden, "Keine Schreibrechte für die Zielnotiz")
return
}
var req struct {
SourceID string `json:"sourceId"`
ClientID string `json:"clientId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.SourceID == "" {
writeError(w, http.StatusBadRequest, "sourceId fehlt")
return
}
sourceUsers, _ := s.store.GetAuthorizedDocUsers(req.SourceID)
res, err := s.store.MergeDocuments(userID, req.SourceID, targetID, s.restructureActor(r, req.ClientID), s.leases.View(targetID))
if err != nil {
writeError(w, restructureStatus(err), err.Error())
return
}
s.notifyDocChanged(targetID, s.store.DocHead(targetID))
for _, uid := range sourceUsers {
s.wsHub.BroadcastToUser(uid, "DOC_DELETED", map[string]string{"id": req.SourceID})
}
writeJSON(w, http.StatusOK, res)
}
// handleUndoMerge: POST /documents/{id}/merge/undo {sourceId, headingId, clientId}
func (s *Server) handleUndoMerge(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
targetID := chi.URLParam(r, "id")
if !storage.CanEditDoc(s.store.DocRole(userID, targetID)) {
writeError(w, http.StatusForbidden, "Keine Schreibrechte für die Zielnotiz")
return
}
var req struct {
SourceID string `json:"sourceId"`
HeadingID string `json:"headingId"`
ClientID string `json:"clientId"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.SourceID == "" || req.HeadingID == "" {
writeError(w, http.StatusBadRequest, "sourceId und headingId fehlen")
return
}
m := storage.MergeResult{TargetID: targetID, SourceID: req.SourceID, HeadingID: req.HeadingID}
if err := s.store.UndoMerge(userID, m, s.restructureActor(r, req.ClientID), s.leases.View(targetID)); err != nil {
writeError(w, restructureStatus(err), err.Error())
return
}
s.notifyDocChanged(targetID, s.store.DocHead(targetID))
s.broadcastDocToAuthorizedUsers(req.SourceID, "DOC_CREATED")
writeJSON(w, http.StatusOK, map[string]bool{"undone": true})
}