package client import ( "context" "fmt" "io" "log/slog" "strings" "git.franklin.lab/steve.cliff/pcli/model" ) // ImportProgress tracks creation counts during import. type ImportProgress struct { Boards int Lists int Labels int Cards int TaskLists int Tasks int Comments int } func (p ImportProgress) String() string { return fmt.Sprintf("boards: %d, lists: %d, labels: %d, cards: %d, task lists: %d, tasks: %d, comments: %d", p.Boards, p.Lists, p.Labels, p.Cards, p.TaskLists, p.Tasks, p.Comments) } func (c *Client) ImportProject(ctx context.Context, export *model.ExportEnvelope, stderr io.Writer) (*ImportProgress, error) { progress := &ImportProgress{} // Resolve or create project projectID, err := c.resolveOrCreateProject(ctx, export.Project.Name, export.Project.Description) if err != nil { return nil, fmt.Errorf("failed to resolve/create project: %w", err) } // Check for board name conflicts before creating anything if err := c.checkBoardConflicts(ctx, projectID, export.Project); err != nil { return nil, err } // Import each board for _, board := range export.Project.Boards { fmt.Fprintf(stderr, "Importing board %q...\n", board.Name) if err := c.importBoard(ctx, projectID, board, progress); err != nil { return progress, fmt.Errorf("failed to import board %q: %w", board.Name, err) } } return progress, nil } func (c *Client) resolveOrCreateProject(ctx context.Context, name string, description *string) (string, error) { projects, err := c.ListProjects(ctx) if err != nil { return "", fmt.Errorf("failed to list projects: %w", err) } for _, p := range projects { if strings.EqualFold(p.Name, name) { c.Logger.Info("Using existing project", slog.String("project", name), slog.String("id", p.ID)) return p.ID, nil } } // Create new project fields := ProjectCreateFields{ Type: "private", Name: name, } if description != nil { fields.Description = description } project, err := c.CreateProject(ctx, fields) if err != nil { return "", fmt.Errorf("failed to create project: %w", err) } c.Logger.Info("Created project", slog.String("project", name), slog.String("id", project.ID)) return project.ID, nil } func (c *Client) checkBoardConflicts(ctx context.Context, projectID string, export model.ExportProject) error { boards, err := c.ListBoards(ctx) if err != nil { return fmt.Errorf("failed to list boards: %w", err) } existingNames := make(map[string]bool) for _, b := range boards { if b.ProjectID == projectID { existingNames[strings.ToLower(b.Name)] = true } } var conflicts []string for _, b := range export.Boards { if existingNames[strings.ToLower(b.Name)] { conflicts = append(conflicts, b.Name) } } if len(conflicts) > 0 { return fmt.Errorf("board(s) already exist in project %q: %s", export.Name, strings.Join(conflicts, ", ")) } return nil } func (c *Client) importBoard(ctx context.Context, projectID string, board model.ExportBoard, progress *ImportProgress) error { // Create board createdBoard, err := c.CreateBoard(ctx, projectID, BoardCreateFields{ Name: board.Name, Position: board.Position, }) if err != nil { return fmt.Errorf("failed to create board: %w", err) } progress.Boards++ // Create lists, build name -> ID map (skip system lists) listMap := make(map[string]string) for _, l := range board.Lists { if l.Type == "archive" || l.Type == "trash" { continue } created, err := c.CreateList(ctx, createdBoard.ID, ListCreateFields{ Name: l.Name, Position: l.Position, Type: l.Type, }) if err != nil { return fmt.Errorf("failed to create list %q: %w", l.Name, err) } listMap[l.Name] = created.ID progress.Lists++ } // Create labels, build name -> ID map // Labels are keyed by name only (Planka scopes labels to a board) labelMap := make(map[string]string) for _, l := range board.Labels { fields := map[string]any{ "position": l.Position, "color": l.Color, } if l.Name != nil { fields["name"] = *l.Name } created, err := c.CreateLabel(ctx, createdBoard.ID, fields) if err != nil { return fmt.Errorf("failed to create label: %w", err) } name := "" if l.Name != nil { name = *l.Name } labelMap[name] = created.ID progress.Labels++ } // Create cards for _, card := range board.Cards { listID, ok := listMap[card.ListName] if !ok { return fmt.Errorf("card %q references unknown list %q", card.Name, card.ListName) } cardFields := map[string]any{ "name": card.Name, "type": card.Type, } if card.Position != nil { cardFields["position"] = *card.Position } if card.Description != nil { cardFields["description"] = *card.Description } if card.DueDate != nil { cardFields["dueDate"] = *card.DueDate } if card.IsClosed { cardFields["isClosed"] = true } createdCard, err := c.CreateCard(ctx, listID, cardFields) if err != nil { return fmt.Errorf("failed to create card %q: %w", card.Name, err) } progress.Cards++ // Add card labels for _, labelName := range card.LabelNames { labelID, ok := labelMap[labelName] if !ok { c.Logger.Warn("Label not found for card, skipping", slog.String("card", card.Name), slog.String("label", labelName), ) continue } if err := c.AddCardLabel(ctx, createdCard.ID, labelID); err != nil { return fmt.Errorf("failed to add label %q to card %q: %w", labelName, card.Name, err) } } // Create task lists and tasks for _, tl := range card.TaskLists { tlFields := map[string]any{ "name": tl.Name, "position": tl.Position, } createdTL, err := c.CreateTaskList(ctx, createdCard.ID, tlFields) if err != nil { return fmt.Errorf("failed to create task list %q: %w", tl.Name, err) } progress.TaskLists++ for _, t := range tl.Tasks { tFields := map[string]any{ "name": t.Name, "position": t.Position, "isCompleted": t.IsCompleted, } if _, err := c.CreateTask(ctx, createdTL.ID, tFields); err != nil { return fmt.Errorf("failed to create task %q: %w", t.Name, err) } progress.Tasks++ } } // Create comments for _, cm := range card.Comments { if _, err := c.CreateComment(ctx, createdCard.ID, cm.Text); err != nil { return fmt.Errorf("failed to create comment on card %q: %w", card.Name, err) } progress.Comments++ } } return nil }