MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m2s
The server only creates the new note and returns the link; the editor swaps the branch for it as an ordinary own edit through the sync engine. No more clash with unsent typing (conflict copies), the link shows at once also with polling, and Strg+Z restores the branch. Undoing (toast or Strg+Z) puts the new note into the trash via POST /extract/undo. The sidebar gets the new note right away and opens its folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
5.0 KiB
Go
138 lines
5.0 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, link, 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
|
|
}
|
|
if full, err := s.store.GetDocumentByID(userID, created.ID); err == nil {
|
|
created = full
|
|
}
|
|
s.broadcastDocToAuthorizedUsers(created.ID, "DOC_CREATED")
|
|
writeJSON(w, http.StatusCreated, map[string]interface{}{"document": created, "link": link})
|
|
}
|
|
|
|
// handleUndoExtract: POST /documents/{id}/extract/undo puts the extracted note {id} into the trash.
|
|
func (s *Server) handleUndoExtract(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
users, _ := s.store.GetAuthorizedDocUsers(docID)
|
|
if err := s.store.UndoExtract(userID, docID); err != nil {
|
|
writeError(w, restructureStatus(err), err.Error())
|
|
return
|
|
}
|
|
for _, uid := range users {
|
|
s.wsHub.BroadcastToUser(uid, "DOC_DELETED", map[string]string{"id": docID})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]bool{"undone": true})
|
|
}
|
|
|
|
// 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})
|
|
}
|