22d5848e1a
- Introduced `openspec-sync-specs` skill to sync delta specs to main specs, allowing intelligent merging of requirements. - Added `openspec-verify-change` skill to verify implementation against change artifacts, ensuring completeness, correctness, and coherence before archiving. docs: Create CLAUDE.md for project guidance - Added CLAUDE.md to provide an overview of the PCLI project, including build, test commands, architecture, and resource addition guidelines. chore: Add new change and design documents for project filter in status command - Created `.openspec.yaml`, `design.md`, `proposal.md`, and `tasks.md` for the `add-project-filter-to-status` change. - Updated specs for CLI commands and status command to include project filtering functionality. feat: Expand board included parsing in API client - Added parsing for `labels`, `cardLabels`, and `cardMemberships` in the `GetBoard` response. - Updated `ListCardsByBoard` to enrich card output with label names, enhancing usability in kanban sync workflows.
284 lines
6.6 KiB
Go
284 lines
6.6 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"git.franklin.lab/steve.cliff/pcli/model"
|
|
)
|
|
|
|
func (c *Client) GetCard(ctx context.Context, id string) (*model.CardDetail, error) {
|
|
data, err := c.DoNoBody(ctx, "GET", fmt.Sprintf("/api/cards/%s", id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response struct {
|
|
Item model.Card `json:"item"`
|
|
Included struct {
|
|
TaskLists []model.TaskList `json:"taskLists"`
|
|
Tasks []model.Task `json:"tasks"`
|
|
} `json:"included"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal card response: %w", err)
|
|
}
|
|
|
|
result := &model.CardDetail{
|
|
Card: response.Item,
|
|
TaskLists: response.Included.TaskLists,
|
|
Tasks: response.Included.Tasks,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateCard(ctx context.Context, listId string, fields map[string]any) (*model.Card, error) {
|
|
data, err := c.Do(ctx, "POST", fmt.Sprintf("/api/lists/%s/cards", listId), fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response struct {
|
|
Item model.Card `json:"item"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal card response: %w", err)
|
|
}
|
|
|
|
return &response.Item, nil
|
|
}
|
|
|
|
func (c *Client) UpdateCard(ctx context.Context, id string, fields map[string]any) (*model.Card, error) {
|
|
data, err := c.Do(ctx, "PATCH", fmt.Sprintf("/api/cards/%s", id), fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response struct {
|
|
Item model.Card `json:"item"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal card response: %w", err)
|
|
}
|
|
|
|
return &response.Item, nil
|
|
}
|
|
|
|
func (c *Client) DeleteCard(ctx context.Context, id string) error {
|
|
_, err := c.DoNoBody(ctx, "DELETE", fmt.Sprintf("/api/cards/%s", id))
|
|
return err
|
|
}
|
|
|
|
func (c *Client) DuplicateCard(ctx context.Context, id string, name *string, position *float64) (*model.Card, error) {
|
|
fields := make(map[string]any)
|
|
if name != nil {
|
|
fields["name"] = *name
|
|
}
|
|
if position != nil {
|
|
fields["position"] = *position
|
|
}
|
|
|
|
data, err := c.Do(ctx, "POST", fmt.Sprintf("/api/cards/%s/duplicate", id), fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response struct {
|
|
Item model.Card `json:"item"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal card response: %w", err)
|
|
}
|
|
|
|
return &response.Item, nil
|
|
}
|
|
|
|
func (c *Client) ListCards(ctx context.Context, listId string, limit int) ([]model.Card, error) {
|
|
var all []model.Card
|
|
var before string
|
|
|
|
for {
|
|
path := fmt.Sprintf("/api/lists/%s/cards", listId)
|
|
if before != "" {
|
|
path = fmt.Sprintf("%s?before=%s", path, before)
|
|
}
|
|
|
|
c.Logger.Debug("Fetching cards page",
|
|
slog.String("listId", listId),
|
|
slog.String("before", before),
|
|
)
|
|
|
|
data, err := c.DoNoBody(ctx, "GET", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response struct {
|
|
Items []model.Card `json:"items"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal cards response: %w", err)
|
|
}
|
|
|
|
if len(response.Items) == 0 {
|
|
break
|
|
}
|
|
|
|
all = append(all, response.Items...)
|
|
|
|
if limit > 0 && len(all) >= limit {
|
|
all = all[:limit]
|
|
break
|
|
}
|
|
|
|
before = response.Items[len(response.Items)-1].ID
|
|
}
|
|
|
|
return all, nil
|
|
}
|
|
|
|
func (c *Client) ListCardActions(ctx context.Context, cardId string, limit int) ([]model.Action, error) {
|
|
var all []model.Action
|
|
var beforeId string
|
|
|
|
for {
|
|
path := fmt.Sprintf("/api/cards/%s/actions", cardId)
|
|
if beforeId != "" {
|
|
path = fmt.Sprintf("%s?beforeId=%s", path, beforeId)
|
|
}
|
|
|
|
c.Logger.Debug("Fetching card actions page",
|
|
slog.String("cardId", cardId),
|
|
slog.String("beforeId", beforeId),
|
|
)
|
|
|
|
data, err := c.DoNoBody(ctx, "GET", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response struct {
|
|
Items []model.Action `json:"items"`
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &response); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal card actions response: %w", err)
|
|
}
|
|
|
|
if len(response.Items) == 0 {
|
|
break
|
|
}
|
|
|
|
all = append(all, response.Items...)
|
|
|
|
if limit > 0 && len(all) >= limit {
|
|
all = all[:limit]
|
|
break
|
|
}
|
|
|
|
beforeId = response.Items[len(response.Items)-1].ID
|
|
}
|
|
|
|
return all, nil
|
|
}
|
|
|
|
func (c *Client) ListCardsByBoard(ctx context.Context, boardId string, limit int) ([]model.CardWithList, error) {
|
|
board, err := c.GetBoard(ctx, boardId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get board: %w", err)
|
|
}
|
|
|
|
listNames := make(map[string]string)
|
|
for _, list := range board.Lists {
|
|
name := ""
|
|
if list.Name != nil {
|
|
name = *list.Name
|
|
}
|
|
listNames[list.ID] = name
|
|
}
|
|
|
|
// Build label ID -> name map
|
|
labelNames := make(map[string]string)
|
|
for _, label := range board.Labels {
|
|
name := ""
|
|
if label.Name != nil {
|
|
name = *label.Name
|
|
}
|
|
labelNames[label.ID] = name
|
|
}
|
|
|
|
// Build card ID -> label names map
|
|
cardLabelNames := make(map[string][]string)
|
|
for _, cl := range board.CardLabels {
|
|
if name, ok := labelNames[cl.LabelID]; ok {
|
|
cardLabelNames[cl.CardID] = append(cardLabelNames[cl.CardID], name)
|
|
}
|
|
}
|
|
|
|
var allCards []model.CardWithList
|
|
for _, card := range board.Cards {
|
|
labels := cardLabelNames[card.ID]
|
|
if labels == nil {
|
|
labels = []string{}
|
|
}
|
|
cardWithList := model.CardWithList{
|
|
Card: card,
|
|
ListName: listNames[card.ListID],
|
|
Labels: labels,
|
|
}
|
|
allCards = append(allCards, cardWithList)
|
|
|
|
if limit > 0 && len(allCards) >= limit {
|
|
return allCards[:limit], nil
|
|
}
|
|
}
|
|
|
|
return allCards, nil
|
|
}
|
|
|
|
func (c *Client) AddCardLabel(ctx context.Context, cardId, labelId string) error {
|
|
fields := map[string]any{
|
|
"labelId": labelId,
|
|
}
|
|
|
|
_, err := c.DoWithFallback(ctx, "POST",
|
|
fmt.Sprintf("/api/cards/%s/card-labels", cardId),
|
|
fmt.Sprintf("/api/cards/%s/labels", cardId),
|
|
fields)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) RemoveCardLabel(ctx context.Context, cardId, labelId string) error {
|
|
_, err := c.DoNoBodyWithFallback(ctx, "DELETE",
|
|
fmt.Sprintf("/api/cards/%s/card-labels/labelId:%s", cardId, labelId),
|
|
fmt.Sprintf("/api/cards/%s/labels/%s", cardId, labelId))
|
|
return err
|
|
}
|
|
|
|
func (c *Client) AddCardMember(ctx context.Context, cardId, userId string) error {
|
|
fields := map[string]any{
|
|
"userId": userId,
|
|
}
|
|
|
|
_, err := c.DoWithFallback(ctx, "POST",
|
|
fmt.Sprintf("/api/cards/%s/card-memberships", cardId),
|
|
fmt.Sprintf("/api/cards/%s/memberships", cardId),
|
|
fields)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) RemoveCardMember(ctx context.Context, cardId, userId string) error {
|
|
_, err := c.DoNoBodyWithFallback(ctx, "DELETE",
|
|
fmt.Sprintf("/api/cards/%s/card-memberships/userId:%s", cardId, userId),
|
|
fmt.Sprintf("/api/cards/%s/memberships/%s", cardId, userId))
|
|
return err
|
|
}
|