// 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 := readStructuredFileMap(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 := readStructuredFileMap(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 := writeCBORFile(settingsPath, settingsPayload); err != nil { return fmt.Errorf("write project %s: %w", posixSettingsFileName, err) } return nil } func (service *Service) projectSettingsPath(projectSlug string) string { return filepath.Join(strings.TrimSpace(service.posixRoot), "projects", slugDir("project", projectSlug), posixSettingsFileName) } 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 } }