MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m56s
- 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>
124 lines
4.5 KiB
Go
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})
|
|
}
|