Files
Work/Backend/internal/bootstrap/project_order.go
T

472 lines
14 KiB
Go

// 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
}
}