Skip to content

feat: Add 'Move to Another Board' to RepoCard overflow menu #133

@ryota-murakami

Description

@ryota-murakami

Summary

Add a "Move to Another Board" menu item to the RepoCard overflow menu (OverflowMenu.tsx), allowing users to transfer a card from the current board to a different board's status column — without deleting and re-adding.

Motivation

Currently users must:

  1. Remove from Board (permanently deletes the card + projectinfo)
  2. Navigate to the target board
  3. Re-add the repository via AddRepositoryCombobox
  4. Re-enter notes, links, and comments from scratch

This is destructive and tedious. A direct move operation preserves all projectinfo (notes, links, comments) while changing only board_id and status_id.

User Story

As a user managing repositories across multiple boards, I want to move a card to another board so that my project notes and links are preserved.

UI Design

OverflowMenu Addition

┌──────────────────────────┐
│ ☐ Open on GitHub         │
│ ☐ Open Production URL    │
│ ────────────────────     │
│ 📦 Move to Maintenance   │
│ 📋 Move to Another Board │  ← NEW
│ ────────────────────     │
│ 🗑 Remove from Board     │
└──────────────────────────┘
  • Position: After "Move to Maintenance", before the destructive separator
  • Icon: ArrowRightLeft from lucide-react
  • Label: "Move to Another Board"
  • Context: board only (not shown in maintenance context)

Board Picker Dialog

When clicked, opens a Dialog with a two-step selector:

┌─────────────────────────────────────────┐
│ Move to Another Board                   │
│ ─────────────────────────────────────── │
│                                         │
│ Select destination board:               │
│ ┌─────────────────────────────────────┐ │
│ │ 🔲 Work Projects                    │ │
│ │ 🔲 Personal OSS                     │ │
│ │ 🔲 Learning & Research              │ │
│ └─────────────────────────────────────┘ │
│                                         │
│ Select column:                          │
│ ┌─────────────────────────────────────┐ │
│ │ 🟢 Pending                          │ │
│ │ 🟡 In Progress                      │ │
│ │ 🔵 Done                             │ │
│ └─────────────────────────────────────┘ │
│                                         │
│ ─────────────────────────────────────── │
│                        [Cancel] [Move]  │
└─────────────────────────────────────────┘

Behavior:

  • Current board is excluded from the board list
  • Board selection auto-selects the first column of that board
  • Column list shows colored dots matching column color
  • Move button is disabled until both board and column are selected
  • On success: card disappears from current board (optimistic), toast "Moved to {boardName}"
  • On error: toast with error message, card remains in place

Architecture

Files to Create / Modify

File Action Description
supabase/migrations/YYYYMMDD_move_card_to_board.sql Create RPC function move_card_to_board
src/lib/actions/repo-cards.ts Modify Add moveCardToBoard() server action
src/components/Board/MoveToAnotherBoardDialog.tsx Create Board + column picker dialog
src/components/Board/OverflowMenu.tsx Modify Add menu item + dialog trigger
src/components/Board/RepoCard.tsx Modify Pass onMoveToBoard callback
src/components/Board/KanbanBoard.tsx Modify Handle card removal after move
src/app/board/[id]/BoardPageClient.tsx Modify Wire handleMoveToBoard callback
e2e/logged-in/move-to-another-board.spec.ts Create E2E tests
src/tests/unit/components/Board/MoveToAnotherBoardDialog.test.tsx Create Unit tests

Database: RPC Function

CREATE OR REPLACE FUNCTION move_card_to_board(
  p_card_id UUID,
  p_target_board_id UUID,
  p_target_status_id UUID
)
RETURNS VOID
LANGUAGE plpgsql
SECURITY INVOKER
SET search_path = pg_catalog, public
AS $$
DECLARE
  v_next_order INTEGER;
BEGIN
  -- Calculate next order in target status column
  SELECT COALESCE(MAX("order"), -1) + 1 INTO v_next_order
  FROM public.repocard
  WHERE status_id = p_target_status_id;

  -- Atomic update: change board_id, status_id, order
  UPDATE public.repocard
  SET board_id = p_target_board_id,
      status_id = p_target_status_id,
      "order" = v_next_order,
      updated_at = now()
  WHERE id = p_card_id;

  IF NOT FOUND THEN
    RAISE EXCEPTION 'repocard % not found', p_card_id;
  END IF;
END;
$$;

Key design decisions:

  • UPDATE, not DELETE+INSERT: Preserves repocard.idprojectinfo.repo_card_id FK stays valid automatically
  • SECURITY INVOKER respects RLS (user must own both boards)
  • UNIQUE constraint (board_id, repo_owner, repo_name) will naturally raise an error if duplicate exists → catch in server action

Server Action

// lib/actions/repo-cards.ts
export async function moveCardToBoard(
  cardId: string,
  targetBoardId: string,
  targetStatusId: string,
): Promise<ActionResult<{ cardId: string }>>

Validation:

  1. Validate all 3 UUIDs via uuidSchema
  2. Verify target board ownership (RLS handles this, but explicit check for better error message)
  3. Verify target status belongs to target board
  4. Check for duplicate (board_id, repo_owner, repo_name) before calling RPC
  5. Call move_card_to_board RPC
  6. Return { success: true, data: { cardId } }

Component: MoveToAnotherBoardDialog

interface MoveToAnotherBoardDialogProps {
  isOpen: boolean
  onClose: () => void
  cardId: string
  cardTitle: string
  currentBoardId: string
  onMoveSuccess: (cardId: string) => void
}

State flow:

  1. On open → call getUserBoardsWithStatusLists() (lazy load)
  2. Filter out currentBoardId from board list
  3. User selects board → populate column list
  4. User selects column → enable Move button
  5. On Move → call moveCardToBoard(cardId, boardId, statusId)
  6. On success → onMoveSuccess(cardId), close dialog, toast

Redux Integration

In BoardPageClient.tsx:

const handleMoveToBoard = useCallback(async (cardId: string) => {
  // After successful move, remove card from current board's Redux state
  dispatch(removeRepoCard(cardId))
}, [dispatch])

Acceptance Criteria

  • "Move to Another Board" appears in OverflowMenu (board context only)
  • Dialog shows all user's boards except the current one
  • Dialog shows status columns for the selected board with color indicators
  • Move button is disabled until both board and column are selected
  • Successful move: card disappears from current board, toast confirmation
  • ProjectInfo (notes, links, comments) is preserved after move
  • Duplicate detection: shows error if repo already exists on target board
  • Loading state during board fetch and move operation
  • Works with keyboard navigation (tab through boards, columns, Move button)
  • Accessibility: proper ARIA labels, focus management
  • E2E test: move card → verify card on target board → verify projectinfo preserved
  • Unit test: dialog rendering, selection state, error handling

Edge Cases

Case Expected Behavior
User has only 1 board Menu item disabled with tooltip "No other boards available"
Target board has same repo Error toast: "Repository already exists on {boardName}"
Board deleted while dialog open Error toast: "Board not found"
Network error during move Error toast, card stays on current board
Card moved while dialog open (race) RPC raises error, toast notification

Test Plan

E2E (e2e/logged-in/move-to-another-board.spec.ts)

  1. Menu item visible in overflow menu
  2. Dialog opens with board list (current board excluded)
  3. Column list populates on board selection
  4. Successful move → card removed from source board
  5. Navigate to target board → card visible in correct column
  6. ProjectInfo preserved after move (verify via DB query)
  7. Duplicate prevention (add same repo to target board first, then try to move)

Unit Tests

  1. Dialog rendering (open/close, board list, column list)
  2. Board filtering (current board excluded)
  3. Selection state (disabled button, enabled after selection)
  4. Error state rendering
  5. Loading state rendering

Dependencies

  • Existing: getUserBoardsWithStatusLists() (repo-cards.ts:427)
  • Existing: ActionResult<T> pattern
  • Existing: OverflowMenu component
  • New: move_card_to_board RPC function (migration)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions