Fix: persist folder drag and drop moves

This commit is contained in:
MangoPig
2026-06-24 17:59:43 +01:00
parent a92e188f84
commit 9ddfa0c3c7
6 changed files with 578 additions and 6 deletions
@@ -26,6 +26,11 @@ type deleteProjectFolderRequest struct {
FolderID string `json:"folderId"`
}
type moveProjectFolderRequest struct {
FolderID string `json:"folderId"`
ParentFolderID string `json:"parentFolderId"`
}
func (routes apiRoutes) handleProjectFolders(w http.ResponseWriter, r *http.Request) {
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
if projectID == "" {
@@ -161,6 +166,44 @@ func (routes apiRoutes) handleRenameProjectFolder(w http.ResponseWriter, r *http
})
}
func (routes apiRoutes) handleMoveProjectFolder(w http.ResponseWriter, r *http.Request) {
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
if projectID == "" {
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
return
}
payload, ok := decodeMoveProjectFolderRequest(w, r)
if !ok {
return
}
payload.FolderID = strings.TrimSpace(payload.FolderID)
payload.ParentFolderID = strings.TrimSpace(payload.ParentFolderID)
if payload.FolderID == "" {
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
return
}
result, err := routes.bootstrapService().MoveProjectFolder(r.Context(), bootstrapservice.MoveProjectFolderInput{
ProjectID: projectID,
FolderID: payload.FolderID,
ParentFolderID: payload.ParentFolderID,
})
if err != nil {
routes.writeProjectFolderError(w, r, err, "move")
return
}
WriteJSON(w, http.StatusOK, map[string]any{
"data": result,
"meta": map[string]any{
"resource": "project-folder-move",
"persisted": true,
},
})
}
func (routes apiRoutes) handleProjectTreeFolders(w http.ResponseWriter, r *http.Request) {
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
if projectID == "" {
@@ -296,10 +339,50 @@ func (routes apiRoutes) handleRenameProjectTreeFolder(w http.ResponseWriter, r *
})
}
func (routes apiRoutes) handleMoveProjectTreeFolder(w http.ResponseWriter, r *http.Request) {
projectID := strings.TrimSpace(chi.URLParam(r, "projectId"))
if projectID == "" {
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Project ID is required.")
return
}
payload, ok := decodeMoveProjectFolderRequest(w, r)
if !ok {
return
}
payload.FolderID = strings.TrimSpace(payload.FolderID)
payload.ParentFolderID = strings.TrimSpace(payload.ParentFolderID)
if payload.FolderID == "" {
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", "Folder ID is required.")
return
}
result, err := routes.bootstrapService().MoveProjectTreeFolder(r.Context(), bootstrapservice.MoveProjectFolderInput{
ProjectID: projectID,
FolderID: payload.FolderID,
ParentFolderID: payload.ParentFolderID,
})
if err != nil {
routes.writeProjectFolderError(w, r, err, "move")
return
}
WriteJSON(w, http.StatusOK, map[string]any{
"data": result,
"meta": map[string]any{
"resource": "project-tree-folder-move",
"persisted": true,
},
})
}
func (routes apiRoutes) writeProjectFolderError(w http.ResponseWriter, r *http.Request, err error, operation string) {
switch {
case errors.Is(err, bootstrapservice.ErrProjectNotFound), errors.Is(err, bootstrapservice.ErrProjectFolderNotFound):
WriteError(w, http.StatusNotFound, RequestIDFromContext(r.Context()), "not_found", err.Error())
case errors.Is(err, bootstrapservice.ErrInvalidProjectFolderMove):
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_request", err.Error())
default:
routes.cfg.Logger.Error(operation+" project folder", "error", err, "path", r.URL.Path)
message := "Failed to " + operation + " project folder."
@@ -310,6 +393,30 @@ func (routes apiRoutes) writeProjectFolderError(w http.ResponseWriter, r *http.R
}
}
func decodeMoveProjectFolderRequest(w http.ResponseWriter, r *http.Request) (moveProjectFolderRequest, bool) {
var payload moveProjectFolderRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&payload); err != nil {
if errors.Is(err, io.EOF) {
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_json", "The request body is required and must be valid JSON.")
return payload, false
}
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_json", "The request body must be valid JSON.")
return payload, false
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
WriteError(w, http.StatusBadRequest, RequestIDFromContext(r.Context()), "invalid_json", "The request body must contain a single JSON object.")
return payload, false
}
return payload, true
}
func decodeDeleteProjectFolderRequest(r *http.Request) deleteProjectFolderRequest {
return deleteProjectFolderRequest{
FolderID: strings.TrimSpace(r.URL.Query().Get("folderId")),