Files
vibetree/internal/web/handlers_folders.go
T
Owen f3a959334f Rename project from inventree to vibetree
- go.mod module path (and every internal/vibetree import) inventree -> vibetree
- cmd/inventree/ -> cmd/vibetree/, deploy/inventree.service -> vibetree.service
- Default site name (seed data + the siteName template func's fallback)
  is now "vibetree", shown in the navbar, <title>, and login page
- Session cookie renamed inventree_session -> vibetree_session
- Default DB path data/inventree.db -> data/vibetree.db, CLI/log text,
  Dockerfile, docker-compose.yml, README, .gitignore/.dockerignore all
  updated to match

Existing installs: this changes the session cookie name (users will be
signed out once) and the default DB filename (point -db at your existing
file, or rename it, to keep your data).
2026-08-17 17:14:52 -05:00

226 lines
5.4 KiB
Go

package web
import (
"errors"
"net/http"
"vibetree/internal/folders"
"vibetree/internal/models"
)
// handleFolderOptions returns every folder as a flat, indented list for
// generic "move to..." pickers (e.g. the asset move context-menu action).
func (a *App) handleFolderOptions(w http.ResponseWriter, r *http.Request) {
opts, err := a.folderOptions(r.Context())
if err != nil {
jsonErr(w, http.StatusInternalServerError, err)
return
}
jsonOK(w, opts)
}
func (a *App) handleFolderGet(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r, "id")
if err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
f, err := a.Folders.Get(r.Context(), id)
if err != nil {
jsonErr(w, http.StatusNotFound, err)
return
}
jsonOK(w, f)
}
type folderCreateRequest struct {
Name string `json:"name"`
ParentID *int64 `json:"parent_id"`
Description string `json:"description"`
Icon string `json:"icon"`
SortOrder int `json:"sort_order"`
}
func (a *App) handleFolderCreate(w http.ResponseWriter, r *http.Request) {
var req folderCreateRequest
if err := readJSON(r, &req); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
f := &models.Folder{Name: req.Name, ParentID: req.ParentID, Description: req.Description, Icon: req.Icon, SortOrder: req.SortOrder}
if err := a.Folders.Create(r.Context(), f); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
jsonOK(w, f)
}
type folderUpdateRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
SortOrder int `json:"sort_order"`
}
func (a *App) handleFolderUpdate(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r, "id")
if err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
var req folderUpdateRequest
if err := readJSON(r, &req); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
f, err := a.Folders.Get(r.Context(), id)
if err != nil {
jsonErr(w, http.StatusNotFound, err)
return
}
f.Name, f.Description, f.Icon, f.SortOrder = req.Name, req.Description, req.Icon, req.SortOrder
if err := a.Folders.Update(r.Context(), f); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
jsonOK(w, f)
}
type folderMoveRequest struct {
ParentID *int64 `json:"parent_id"`
}
func (a *App) handleFolderMove(w http.ResponseWriter, r *http.Request) {
id, err := pathID(r, "id")
if err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
var req folderMoveRequest
if err := readJSON(r, &req); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
if err := a.Folders.Move(r.Context(), id, req.ParentID); err != nil {
status := http.StatusBadRequest
if errors.Is(err, folders.ErrInvalidParent) {
status = http.StatusConflict
}
jsonErr(w, status, err)
return
}
jsonOK(w, map[string]any{"id": id, "parent_id": req.ParentID})
}
func (a *App) handleFolderDeleteInfo(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := pathID(r, "id")
if err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
folderCount, assetCount, err := a.Folders.Counts(ctx, id)
if err != nil {
jsonErr(w, http.StatusInternalServerError, err)
return
}
f, err := a.Folders.Get(ctx, id)
if err != nil {
jsonErr(w, http.StatusNotFound, err)
return
}
all, err := a.Folders.All(ctx)
if err != nil {
jsonErr(w, http.StatusInternalServerError, err)
return
}
descendants := map[int64]bool{id: true}
changed := true
for changed {
changed = false
for _, cand := range all {
if cand.ParentID != nil && descendants[*cand.ParentID] && !descendants[cand.ID] {
descendants[cand.ID] = true
changed = true
}
}
}
byID := map[int64]models.Folder{}
for _, cand := range all {
byID[cand.ID] = cand
}
var pathOf func(int64) string
pathOf = func(fid int64) string {
ff, ok := byID[fid]
if !ok {
return ""
}
if ff.ParentID == nil {
return ff.Name
}
return pathOf(*ff.ParentID) + " / " + ff.Name
}
type dest struct {
ID int64 `json:"id"`
Path string `json:"path"`
}
destinations := []dest{{ID: 0, Path: "Inventory (top level)"}}
for _, cand := range all {
if descendants[cand.ID] {
continue
}
destinations = append(destinations, dest{ID: cand.ID, Path: pathOf(cand.ID)})
}
jsonOK(w, map[string]any{
"folder_name": f.Name,
"folder_count": folderCount,
"asset_count": assetCount,
"destinations": destinations,
})
}
func (a *App) handleFolderDelete(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := pathID(r, "id")
if err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
mode := r.URL.Query().Get("mode")
switch mode {
case "relocate":
var dest *int64
if raw := r.URL.Query().Get("dest"); raw != "" && raw != "0" {
d, err := queryOptionalID(r, "dest")
if err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
dest = d
}
if err := a.Folders.DeleteAndRelocate(ctx, id, dest); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
case "cascade":
if err := a.Folders.CascadeDelete(ctx, id); err != nil {
jsonErr(w, http.StatusBadRequest, err)
return
}
default:
if err := a.Folders.Delete(ctx, id); err != nil {
status := http.StatusBadRequest
if errors.Is(err, folders.ErrNotEmpty) {
status = http.StatusConflict
}
jsonErr(w, status, err)
return
}
}
jsonOK(w, map[string]any{"id": id})
}