Refactor: improve backend modularity
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
// Path: Backend/internal/bootstrap/project_order.go
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (service *Service) readProjectFolderOrder(projectSlug, rootProjectionPath string) map[string][]string {
|
||||
settingsPath := service.projectSettingsPath(projectSlug)
|
||||
settingsPayload := readJSONFileMap(settingsPath)
|
||||
folderOrderPayload, _ := settingsPayload["folderOrder"].(map[string]any)
|
||||
if folderOrderPayload == nil { return map[string][]string{} }
|
||||
scopePayload, _ := folderOrderPayload[projectFolderOrderScope(rootProjectionPath)].(map[string]any)
|
||||
if scopePayload == nil { return map[string][]string{} }
|
||||
byParentPayload, _ := scopePayload["byParent"].(map[string]any)
|
||||
if byParentPayload == nil { return map[string][]string{} }
|
||||
order := make(map[string][]string, len(byParentPayload))
|
||||
for key, raw := range byParentPayload {
|
||||
for _, id := range stringSliceValue(raw) {
|
||||
trimmedID := strings.TrimSpace(id)
|
||||
if trimmedID == "" || slicesContains(order[key], trimmedID) { continue }
|
||||
order[key] = append(order[key], trimmedID)
|
||||
}
|
||||
}
|
||||
return order
|
||||
}
|
||||
|
||||
func (service *Service) writeProjectFolderOrder(projectSlug, rootProjectionPath string, folderOrder map[string][]string) error {
|
||||
settingsPath := service.projectSettingsPath(projectSlug)
|
||||
settingsPayload := readJSONFileMap(settingsPath)
|
||||
if settingsPayload == nil { settingsPayload = map[string]any{} }
|
||||
folderOrderPayload, _ := settingsPayload["folderOrder"].(map[string]any)
|
||||
if folderOrderPayload == nil { folderOrderPayload = map[string]any{} }
|
||||
scopeKey := projectFolderOrderScope(rootProjectionPath)
|
||||
scopePayload, _ := folderOrderPayload[scopeKey].(map[string]any)
|
||||
if scopePayload == nil { scopePayload = map[string]any{} }
|
||||
byParentPayload := map[string]any{}
|
||||
for key, ids := range folderOrder {
|
||||
if len(ids) == 0 { continue }
|
||||
copied := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
trimmedID := strings.TrimSpace(id)
|
||||
if trimmedID == "" || slicesContains(copied, trimmedID) { continue }
|
||||
copied = append(copied, trimmedID)
|
||||
}
|
||||
if len(copied) > 0 { byParentPayload[key] = copied }
|
||||
}
|
||||
scopePayload["byParent"] = byParentPayload
|
||||
folderOrderPayload[scopeKey] = scopePayload
|
||||
settingsPayload["folderOrder"] = folderOrderPayload
|
||||
if err := writeJSONFile(settingsPath, settingsPayload); err != nil { return fmt.Errorf("write project settings.json: %w", err) }
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *Service) projectSettingsPath(projectSlug string) string {
|
||||
return filepath.Join(strings.TrimSpace(service.posixRoot), "projects", slugDir("project", projectSlug), "settings.json")
|
||||
}
|
||||
|
||||
func projectFolderOrderScope(rootProjectionPath string) string {
|
||||
if strings.HasSuffix(rootProjectionPath, "/tree") { return projectFolderOrderTree }
|
||||
return projectFolderOrderHierarchy
|
||||
}
|
||||
|
||||
func applyProjectHierarchyFolderOrdering(folders []ProjectHierarchyFolderRecord, folderOrder map[string][]string) []ProjectHierarchyFolderRecord {
|
||||
return applyProjectHierarchyFolderOrderingForParent(folders, "", folderOrder)
|
||||
}
|
||||
|
||||
func applyProjectHierarchyFolderOrderingForParent(folders []ProjectHierarchyFolderRecord, parentID string, folderOrder map[string][]string) []ProjectHierarchyFolderRecord {
|
||||
if len(folders) == 0 { return folders }
|
||||
nextFolders := make([]ProjectHierarchyFolderRecord, len(folders))
|
||||
copy(nextFolders, folders)
|
||||
for index := range nextFolders { nextFolders[index].Children = applyProjectHierarchyFolderOrderingForParent(nextFolders[index].Children, nextFolders[index].ID, folderOrder) }
|
||||
orderIDs := folderOrder[projectFolderOrderParentKey(parentID)]
|
||||
if len(orderIDs) == 0 { return nextFolders }
|
||||
rankByID := make(map[string]int, len(orderIDs))
|
||||
for index, id := range orderIDs { if _, exists := rankByID[id]; !exists { rankByID[id] = index } }
|
||||
sort.SliceStable(nextFolders, func(left, right int) bool {
|
||||
leftRank, leftOrdered := rankByID[nextFolders[left].ID]
|
||||
rightRank, rightOrdered := rankByID[nextFolders[right].ID]
|
||||
if leftOrdered && rightOrdered { return leftRank < rightRank }
|
||||
if leftOrdered != rightOrdered { return leftOrdered }
|
||||
return false
|
||||
})
|
||||
return nextFolders
|
||||
}
|
||||
|
||||
func removeFolderOrder(folderOrder map[string][]string, folderID string) {
|
||||
removeFolderOrderReference(folderOrder, folderID)
|
||||
trimmedFolderID := strings.TrimSpace(folderID)
|
||||
if trimmedFolderID == "" { return }
|
||||
delete(folderOrder, projectFolderOrderParentKey(trimmedFolderID))
|
||||
}
|
||||
|
||||
func removeFolderOrderReference(folderOrder map[string][]string, folderID string) {
|
||||
trimmedFolderID := strings.TrimSpace(folderID)
|
||||
if trimmedFolderID == "" { return }
|
||||
for key, ids := range folderOrder {
|
||||
nextIDs := ids[:0]
|
||||
for _, id := range ids {
|
||||
if strings.TrimSpace(id) == trimmedFolderID { continue }
|
||||
nextIDs = append(nextIDs, id)
|
||||
}
|
||||
if len(nextIDs) == 0 { delete(folderOrder, key); continue }
|
||||
folderOrder[key] = append([]string(nil), nextIDs...)
|
||||
}
|
||||
}
|
||||
|
||||
func insertFolderOrder(folderOrder map[string][]string, parentID, folderID string, index int) {
|
||||
trimmedFolderID := strings.TrimSpace(folderID)
|
||||
if trimmedFolderID == "" { return }
|
||||
removeFolderOrderReference(folderOrder, trimmedFolderID)
|
||||
parentKey := projectFolderOrderParentKey(parentID)
|
||||
children := append([]string(nil), folderOrder[parentKey]...)
|
||||
if index < 0 { index = 0 }
|
||||
if index > len(children) { index = len(children) }
|
||||
children = slicesInsert(children, index, trimmedFolderID)
|
||||
folderOrder[parentKey] = children
|
||||
}
|
||||
|
||||
func seedFolderOrderParent(folderOrder map[string][]string, folders []ProjectHierarchyFolderRecord, parentID string) {
|
||||
children := folders
|
||||
trimmedParentID := strings.TrimSpace(parentID)
|
||||
if trimmedParentID != "" {
|
||||
parent, found := findProjectHierarchyFolder(folders, trimmedParentID)
|
||||
if !found { return }
|
||||
children = parent.Children
|
||||
}
|
||||
parentKey := projectFolderOrderParentKey(trimmedParentID)
|
||||
seeded := make([]string, 0, len(children))
|
||||
for _, child := range children {
|
||||
childID := strings.TrimSpace(child.ID)
|
||||
if childID == "" || slicesContains(seeded, childID) { continue }
|
||||
seeded = append(seeded, childID)
|
||||
}
|
||||
if len(seeded) == 0 { delete(folderOrder, parentKey); return }
|
||||
folderOrder[parentKey] = seeded
|
||||
}
|
||||
|
||||
func folderOrderChildren(folderOrder map[string][]string, parentID string) []string {
|
||||
return append([]string(nil), folderOrder[projectFolderOrderParentKey(parentID)]...)
|
||||
}
|
||||
|
||||
func projectFolderOrderParentKey(parentID string) string {
|
||||
trimmedParentID := strings.TrimSpace(parentID)
|
||||
if trimmedParentID == "" { return projectFolderOrderRootKey }
|
||||
return trimmedParentID
|
||||
}
|
||||
|
||||
func stringSliceValue(value any) []string {
|
||||
items, ok := value.([]any)
|
||||
if !ok {
|
||||
if typed, ok := value.([]string); ok { return typed }
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(items))
|
||||
for _, item := range items { if text, ok := item.(string); ok { result = append(result, text) } }
|
||||
return result
|
||||
}
|
||||
|
||||
func slicesContains(values []string, value string) bool {
|
||||
for _, existing := range values { if existing == value { return true } }
|
||||
return false
|
||||
}
|
||||
|
||||
func slicesInsert(values []string, index int, value string) []string {
|
||||
values = append(values, "")
|
||||
copy(values[index+1:], values[index:])
|
||||
values[index] = value
|
||||
return values
|
||||
}
|
||||
|
||||
func buildProjectHierarchyFolderTree(rows []projectHierarchyFolderRow, rootParentPath string) []ProjectHierarchyFolderRecord {
|
||||
if len(rows) == 0 { return nil }
|
||||
nodesByPath := make(map[string]*ProjectHierarchyFolderRecord, len(rows))
|
||||
childrenByParent := make(map[string][]string)
|
||||
for _, row := range rows {
|
||||
folderID := strings.TrimSpace(row.ID)
|
||||
if folderID == "" { folderID = row.Path }
|
||||
label := strings.TrimSpace(row.Label)
|
||||
if label == "" { label = fallbackFolderLabel(row.Path) }
|
||||
nodesByPath[row.Path] = &ProjectHierarchyFolderRecord{ID: folderID, Path: row.Path, Label: label, Children: []ProjectHierarchyFolderRecord{}}
|
||||
childrenByParent[row.ParentPath] = append(childrenByParent[row.ParentPath], row.Path)
|
||||
}
|
||||
var build func(parentPath string) []ProjectHierarchyFolderRecord
|
||||
build = func(parentPath string) []ProjectHierarchyFolderRecord {
|
||||
childPaths := childrenByParent[parentPath]
|
||||
if len(childPaths) == 0 { return nil }
|
||||
folders := make([]ProjectHierarchyFolderRecord, 0, len(childPaths))
|
||||
for _, childPath := range childPaths {
|
||||
node := nodesByPath[childPath]
|
||||
if node == nil { continue }
|
||||
folder := ProjectHierarchyFolderRecord{ID: node.ID, Path: node.Path, Label: node.Label, Children: build(filepath.ToSlash(filepath.Join(childPath, "children")))}
|
||||
folders = append(folders, folder)
|
||||
}
|
||||
return folders
|
||||
}
|
||||
return build(rootParentPath)
|
||||
}
|
||||
|
||||
func buildProjectTreeNodeTree(rows []projectTreeNodeRow, rootParentPath string) []ProjectTreeNodeRecord {
|
||||
if len(rows) == 0 { return nil }
|
||||
nodesByPath := make(map[string]*ProjectTreeNodeRecord, len(rows))
|
||||
childrenByParent := make(map[string][]string)
|
||||
for _, row := range rows {
|
||||
nodeKind := normalizeProjectTreeNodeKind(row.Kind)
|
||||
nodeID := strings.TrimSpace(row.ID)
|
||||
if nodeID == "" { nodeID = row.Path }
|
||||
label := strings.TrimSpace(row.Label)
|
||||
if label == "" { if nodeKind == "item" { label = fallbackItemLabel(row.Path) } else { label = fallbackFolderLabel(row.Path) } }
|
||||
nodesByPath[row.Path] = &ProjectTreeNodeRecord{ID: nodeID, Path: row.Path, Label: label, Kind: nodeKind, ItemType: normalizeProjectTreeItemType(row.ItemType), Children: []ProjectTreeNodeRecord{}}
|
||||
parentKey := normalizeProjectTreeParentPath(nodeKind, row.ParentPath)
|
||||
childrenByParent[parentKey] = append(childrenByParent[parentKey], row.Path)
|
||||
}
|
||||
var build func(parentPath string) []ProjectTreeNodeRecord
|
||||
build = func(parentPath string) []ProjectTreeNodeRecord {
|
||||
childPaths := childrenByParent[parentPath]
|
||||
if len(childPaths) == 0 { return nil }
|
||||
nodes := make([]ProjectTreeNodeRecord, 0, len(childPaths))
|
||||
for _, childPath := range childPaths {
|
||||
node := nodesByPath[childPath]
|
||||
if node == nil { continue }
|
||||
nextNode := ProjectTreeNodeRecord{ID: node.ID, Path: node.Path, Label: node.Label, Kind: node.Kind, ItemType: node.ItemType}
|
||||
if node.Kind == "folder" { nextNode.Children = build(node.Path) }
|
||||
nodes = append(nodes, nextNode)
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
return build(rootParentPath)
|
||||
}
|
||||
|
||||
func normalizeProjectTreeParentPath(kind, parentPath string) string {
|
||||
if kind == "folder" && strings.HasSuffix(parentPath, "/children") { return filepath.ToSlash(filepath.Dir(parentPath)) }
|
||||
return parentPath
|
||||
}
|
||||
|
||||
func normalizeProjectTreeNodeKind(kind string) string {
|
||||
switch strings.TrimSpace(kind) {
|
||||
case "item":
|
||||
return "item"
|
||||
case "folder", "hierarchy_folder":
|
||||
return "folder"
|
||||
default:
|
||||
return "folder"
|
||||
}
|
||||
}
|
||||
|
||||
func applyProjectTreeNodeOrdering(nodes []ProjectTreeNodeRecord, folderOrder map[string][]string) []ProjectTreeNodeRecord {
|
||||
return applyProjectTreeNodeOrderingForParent(nodes, "", folderOrder)
|
||||
}
|
||||
|
||||
func applyProjectTreeNodeOrderingForParent(nodes []ProjectTreeNodeRecord, parentID string, folderOrder map[string][]string) []ProjectTreeNodeRecord {
|
||||
if len(nodes) == 0 { return nodes }
|
||||
nextNodes := make([]ProjectTreeNodeRecord, len(nodes))
|
||||
copy(nextNodes, nodes)
|
||||
for index := range nextNodes { if nextNodes[index].Kind == "folder" { nextNodes[index].Children = applyProjectTreeNodeOrderingForParent(nextNodes[index].Children, nextNodes[index].ID, folderOrder) } }
|
||||
orderIDs := folderOrder[projectFolderOrderParentKey(parentID)]
|
||||
if len(orderIDs) == 0 { return nextNodes }
|
||||
rankByID := make(map[string]int, len(orderIDs))
|
||||
for index, id := range orderIDs { if _, exists := rankByID[id]; !exists { rankByID[id] = index } }
|
||||
sort.SliceStable(nextNodes, func(left, right int) bool {
|
||||
leftRank, leftOrdered := rankByID[nextNodes[left].ID]
|
||||
rightRank, rightOrdered := rankByID[nextNodes[right].ID]
|
||||
if leftOrdered && rightOrdered { return leftRank < rightRank }
|
||||
if leftOrdered != rightOrdered { return leftOrdered }
|
||||
return false
|
||||
})
|
||||
return nextNodes
|
||||
}
|
||||
|
||||
func findProjectHierarchyFolder(folders []ProjectHierarchyFolderRecord, folderID string) (ProjectHierarchyFolderRecord, bool) {
|
||||
for _, folder := range folders {
|
||||
if folder.ID == folderID { return folder, true }
|
||||
if child, ok := findProjectHierarchyFolder(folder.Children, folderID); ok { return child, true }
|
||||
}
|
||||
return ProjectHierarchyFolderRecord{}, false
|
||||
}
|
||||
|
||||
func findProjectHierarchyFolderByPath(folders []ProjectHierarchyFolderRecord, folderPath string) (ProjectHierarchyFolderRecord, bool) {
|
||||
for _, folder := range folders {
|
||||
if folder.Path == folderPath { return folder, true }
|
||||
if child, ok := findProjectHierarchyFolderByPath(folder.Children, folderPath); ok { return child, true }
|
||||
}
|
||||
return ProjectHierarchyFolderRecord{}, false
|
||||
}
|
||||
|
||||
func findProjectTreeNode(nodes []ProjectTreeNodeRecord, nodeID string) (ProjectTreeNodeRecord, bool) {
|
||||
for _, node := range nodes {
|
||||
if node.ID == nodeID { return node, true }
|
||||
if child, ok := findProjectTreeNode(node.Children, nodeID); ok { return child, true }
|
||||
}
|
||||
return ProjectTreeNodeRecord{}, false
|
||||
}
|
||||
|
||||
func findProjectTreeNodeByPath(nodes []ProjectTreeNodeRecord, nodePath string) (ProjectTreeNodeRecord, bool) {
|
||||
for _, node := range nodes {
|
||||
if node.Path == nodePath { return node, true }
|
||||
if child, ok := findProjectTreeNodeByPath(node.Children, nodePath); ok { return child, true }
|
||||
}
|
||||
return ProjectTreeNodeRecord{}, false
|
||||
}
|
||||
|
||||
func findProjectTreeFolderByPath(nodes []ProjectTreeNodeRecord, folderPath string) (ProjectTreeNodeRecord, bool) {
|
||||
node, ok := findProjectTreeNodeByPath(nodes, folderPath)
|
||||
if !ok || node.Kind != "folder" { return ProjectTreeNodeRecord{}, false }
|
||||
return node, true
|
||||
}
|
||||
|
||||
func seedProjectTreeOrderParent(folderOrder map[string][]string, nodes []ProjectTreeNodeRecord, parentID string) {
|
||||
children := nodes
|
||||
trimmedParentID := strings.TrimSpace(parentID)
|
||||
if trimmedParentID != "" {
|
||||
parent, found := findProjectTreeNode(nodes, trimmedParentID)
|
||||
if !found || parent.Kind != "folder" { return }
|
||||
children = parent.Children
|
||||
}
|
||||
parentKey := projectFolderOrderParentKey(parentID)
|
||||
if len(folderOrder[parentKey]) > 0 { return }
|
||||
orderedIDs := make([]string, 0, len(children))
|
||||
for _, child := range children {
|
||||
trimmedChildID := strings.TrimSpace(child.ID)
|
||||
if trimmedChildID == "" || slicesContains(orderedIDs, trimmedChildID) { continue }
|
||||
orderedIDs = append(orderedIDs, trimmedChildID)
|
||||
}
|
||||
if len(orderedIDs) > 0 { folderOrder[parentKey] = orderedIDs }
|
||||
}
|
||||
Reference in New Issue
Block a user