Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@ryota-murakami
Copy link
Contributor

@ryota-murakami ryota-murakami commented Jan 27, 2026

Summary

  • Merges ProjectInfoModal into NoteModal for unified note and links editing
  • Users now access both via the Note button instead of separate entry points
  • Removes "Edit Project Info..." from OverflowMenu

Changes

Redux & State

  • Extended draft slice with links field for persistence across browser sessions

Hooks

  • Extended useNoteModal to fetch/save both notes and links
  • Deleted useProjectInfoModal (consolidated)

Components

  • Added Links section to NoteModal (EditableUrlItem, LinkTypeCombobox, CreateLinkTypeDialog)
  • Removed onEdit/onEditProjectInfo props from component chain
  • Deleted ProjectInfoModal component

Tests

  • Updated unit tests, Storybook stories, and E2E tests
  • Deleted tests for removed modules

Test plan

  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test - 1265 tests pass
  • pnpm build succeeds
  • pnpm e2e - 293 tests pass

Files Changed

  • 31 files changed
  • +482/-1901 lines (net reduction of ~1400 lines)
  • 5 files deleted (ProjectInfoModal, useProjectInfoModal, related tests)

Closes #37

Summary by CodeRabbit

  • New Features

    • Note modal now supports saving notes plus multiple links, link presets, per-link validation, and a richer editor; saves include links.
  • UX Improvements

    • Project note editing consolidated into a single Note dialog with updated labels, character-count formatting, and clearer save feedback.
    • “Edit Project Info” pathways removed from menus and card actions, routing users to the unified Note dialog.
  • Tests / Stories

    • Storybook and automated tests updated to reflect the consolidated Note flow and changed modal texts.

✏️ Tip: You can customize this high-level summary in your review settings.

Merge ProjectInfoModal into NoteModal for unified note and links editing.
Users now access both via the Note button instead of separate entry points.

Changes:
- Extend Redux draft slice with links field for persistence
- Extend useNoteModal to fetch/save both notes and links
- Add Links section to NoteModal (EditableUrlItem, LinkTypeCombobox)
- Remove "Edit Project Info..." from OverflowMenu
- Remove onEdit/onEditProjectInfo props from component chain
- Delete deprecated ProjectInfoModal and useProjectInfoModal
- Update unit tests, stories, and E2E tests

Closes #37
@vercel
Copy link
Contributor

vercel bot commented Jan 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
gitbox Ready Ready Preview, Comment Jan 27, 2026 1:07pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Jan 27, 2026

📝 Walkthrough

Walkthrough

Consolidates project metadata editing: removes ProjectInfoModal and its hook, enhances NoteModal to handle notes + links, removes onEdit/onEditProjectInfo props and menu item, updates hooks, stories, tests, and draft state to persist links alongside notes.

Changes

Cohort / File(s) Summary
Modal Removal
components/Modals/ProjectInfoModal.tsx, components/Modals/ProjectInfoModal.stories.tsx
Deleted ProjectInfoModal component and its stories (entire module removed).
NoteModal Enhancement
components/Modals/NoteModal.tsx, components/Modals/NoteModal.stories.tsx
NoteModal now accepts initialLinks: ProjectLink[], manages links state, URL validation, presets, per-URL edit lifecycle, and onSave(note, links) signature. Stories updated to supply initialLinks and new onSave signature.
Hook Migration
hooks/board/useNoteModal.ts, hooks/board/useProjectInfoModal.ts, hooks/board/index.ts
useNoteModal extended to load/save initialLinks and expose save(note, links); useProjectInfoModal deleted and its export removed from hooks/board/index.ts.
Component API & UI
components/Board/KanbanBoard.tsx, components/Board/RepoCard.tsx, components/Board/SortableColumn.tsx, components/Board/StatusColumn.tsx, components/Board/OverflowMenu.tsx
Removed onEditProjectInfo / onEdit props and wiring; RepoCard Enter/key and menus use onNote; OverflowMenu "Edit Project Info…" item and related prop removed.
Stories
components/Board/*.stories.tsx (KanbanBoard, OverflowMenu, RepoCard, SortableColumn, StatusColumn)
Replaced onEdit/onEditProjectInfo story args with onNote across story variants.
App Integration
app/board/[id]/BoardPageClient.tsx, app/maintenance/MaintenanceClient.tsx
Removed ProjectInfoModal imports/wiring; open NoteModal with initialNote and initialLinks via useNoteModal.
Redux / Drafts
lib/redux/slices/draftSlice.ts
Added links: ProjectLink[] to DraftNote, extended updateDraftNote payload, and added selectDraftLinks.
Tests — Unit & E2E
tests/unit/..., e2e/logged-in/... (multiple files)
Removed tests for ProjectInfoModal and useProjectInfoModal; updated unit tests to remove onEdit usage and use onNote; E2E tests updated for NoteModal title/text, character count formatting, toast text, and access via Note button.
CI
.github/workflows/e2e.yml
Graceful handling when per-shard coverage reports are missing (create zeroed summary / early exit).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as "User"
  participant RepoCard as "RepoCard (UI)"
  participant Hook as "useNoteModal"
  participant NoteModal as "NoteModal (UI)"
  participant Redux as "Redux (draftSlice)"
  participant API as "Server API (get/upsertProjectInfo)"

  rect rgba(100,149,237,0.5)
  User->>RepoCard: Click Note button / press Enter
  end

  rect rgba(60,179,113,0.5)
  RepoCard->>Hook: open(cardId)
  Hook->>API: getProjectInfo(cardId)
  API-->>Hook: projectInfo { note, links }
  Hook->>NoteModal: open(initialNote, initialLinks)
  NoteModal-->>User: render editor (note + links)
  end

  rect rgba(255,215,0,0.5)
  User->>NoteModal: Edit note & links → Save
  NoteModal->>Hook: save(note, links)
  Hook->>Redux: updateDraftNote(cardId, note, links)
  Hook->>API: upsertProjectInfo(cardId, { note, links })
  API-->>Hook: success
  Hook->>NoteModal: close()
  NoteModal-->>User: show toast / close
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

Two modals once split in two,
Now Note sings links and text anew.
Menus trimmed and hooks aligned,
One flow for notes and links combined.
A tidy change — concise and kind.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title accurately summarizes the main change: consolidating Note and Link editing into a single NoteModal, removing ProjectInfoModal and onEdit/onEditProjectInfo props.
Linked Issues check ✅ Passed PR fully addresses issue #37: consolidates Note+Link editing into NoteModal, removes onEditProjectInfo from KanbanBoard, removes Edit menu item from OverflowMenu, deletes ProjectInfoModal/useProjectInfoModal.
Out of Scope Changes check ✅ Passed All changes are scoped to consolidating Note and Link editing; only exception is CI fix (coverage upload path) which is incidental and unrelated but minor.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link

codecov-commenter commented Jan 27, 2026

Codecov Report

❌ Patch coverage is 52.30769% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.31%. Comparing base (f38d50e) to head (b38cbd1).

Files with missing lines Patch % Lines
components/Modals/NoteModal.tsx 53.33% 42 Missing ⚠️
app/maintenance/MaintenanceClient.tsx 21.73% 18 Missing ⚠️
lib/redux/slices/draftSlice.ts 71.42% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #39      +/-   ##
==========================================
- Coverage   76.92%   75.31%   -1.61%     
==========================================
  Files         121      119       -2     
  Lines        3904     3849      -55     
  Branches      981      975       -6     
==========================================
- Hits         3003     2899     -104     
- Misses        886      934      +48     
- Partials       15       16       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
e2e/logged-in/maintenance-project-info.spec.ts (1)

94-94: Test name still references ProjectInfoModal.

Line 94's test is named 'should close ProjectInfoModal on Cancel' but the modal is now NoteModal. Update for consistency with the consolidation.

♻️ Suggested fix
-  test('should close ProjectInfoModal on Cancel', async ({ page }) => {
+  test('should close NoteModal on Cancel', async ({ page }) => {
🤖 Fix all issues with AI agents
In `@app/maintenance/MaintenanceClient.tsx`:
- Around line 312-320: openNoteModal currently races with async
getMaintenanceProjectInfo and can overwrite currentNote/currentLinks when the
user switches repos or closes the modal; fix by resetting state on open/close
and guarding the async result: when opening (openNoteModal / setNoteModalOpen)
immediately clear/set currentNote/currentLinks to empty, capture the repo id (or
create an abort token) before calling
startLoadingProjectInfo(getMaintenanceProjectInfo(repo.id)), and when the
promise resolves only call setCurrentNote/setCurrentLinks if the selected repo
id still matches that captured id and the modal is still open; apply the same
guard/reset pattern to the other similar block around lines 737-746.

In `@components/Modals/NoteModal.tsx`:
- Around line 95-133: The effect that resets note/link UI state (useEffect that
calls setNote, setLinks, setEditingUrlIndex, setDeletedLink) is currently
dependent on draft?.content and draft?.links and thus clears editingUrlIndex and
deletedLink on every keystroke; change the dependency list so the reset runs
only when the modal opens or the card changes (e.g., depend on isOpen and cardId
or initialNote/initialLinks) instead of draft?.content/draft?.links, ensuring
editingUrlIndex and deletedLink are only initialized when the modal opens (or
cardId changes) and not on each draft update.
🧹 Nitpick comments (5)
tests/unit/redux/draftSlice.test.ts (2)

47-63: Consider adding a test for backward compatibility.

The reducer preserves existing links when links is not provided in the payload (line 70 of draftSlice.ts), but there's no test verifying this behavior. This is important since the payload has links?: ProjectLink[] (optional).

📝 Suggested test case
it('should preserve existing links when updating content without links', () => {
  const stateWithDraft = {
    ...initialState,
    notes: {
      'card-123': {
        cardId: 'card-123',
        content: 'Old content',
        links: [{ url: 'https://example.com', type: 'docs' }],
        lastModified: mockNow - 1000,
      },
    },
  }

  // Update content without providing links
  const action = updateDraftNote({
    cardId: 'card-123',
    content: 'Updated content',
    // links intentionally omitted
  })

  const nextState = draftSlice(stateWithDraft, action)

  expect(nextState.notes['card-123']!.content).toBe('Updated content')
  // Links should be preserved
  expect(nextState.notes['card-123']!.links).toEqual([
    { url: 'https://example.com', type: 'docs' },
  ])
})

198-227: Consider adding a test for selectDraftLinks selector.

The new selectDraftLinks selector is exported but not tested. Add coverage to ensure it returns the links array or empty array fallback.

📝 Suggested test cases
import {
  // ... existing imports
  selectDraftLinks,
} from '@/lib/redux/slices/draftSlice'

describe('selectDraftLinks', () => {
  it('should return links for specific card', () => {
    const links = [{ url: 'https://example.com', type: 'docs' }]
    const rootState = {
      draft: {
        notes: {
          'card-123': {
            cardId: 'card-123',
            content: 'Test',
            links,
            lastModified: mockNow,
          },
        },
      },
    }

    const selector = selectDraftLinks('card-123')
    expect(selector(rootState)).toEqual(links)
  })

  it('should return empty array for non-existent card', () => {
    const rootState = { draft: initialState }

    const selector = selectDraftLinks('non-existent')
    expect(selector(rootState)).toEqual([])
  })
})
e2e/logged-in/project-info-links.spec.ts (2)

27-44: Consider extracting duplicate openNoteModal helper.

The openNoteModal helper is defined identically in both test describe blocks (lines 27-44 and 460-476). Extract it to the file scope to reduce duplication.

♻️ Suggested refactor
+/**
+ * Helper to open NoteModal from a repo card's Note button
+ */
+async function openNoteModal(
+  page: import('@playwright/test').Page,
+): Promise<import('@playwright/test').Locator> {
+  const card = page.locator('[data-testid^="repo-card-"]').first()
+  await expect(card).toBeVisible({ timeout: 10000 })
+
+  const noteButton = card.getByRole('button', { name: 'Open note' })
+  await expect(noteButton).toBeVisible()
+  await noteButton.click()
+
+  const dialog = page.getByRole('dialog')
+  await expect(dialog).toBeVisible({ timeout: 5000 })
+
+  return dialog
+}
+
 test.describe('ProjectInfo Links (Authenticated)', () => {
   test.use({ storageState: 'e2e/.auth/user.json' })
 
   const BOARD_URL = '/board/board-1'
 
-  /**
-   * Helper to open NoteModal from a repo card's Note button
-   * (Previously opened ProjectInfoModal via overflow menu, now unified into NoteModal)
-   */
-  async function openNoteModal(
-    page: import('@playwright/test').Page,
-  ): Promise<import('@playwright/test').Locator> {
-    // ... implementation
-  }

Also applies to: 460-476


98-98: Fixed waitForTimeout calls may introduce flakiness.

Multiple waitForTimeout(200-400) calls are used for waiting on UI updates. While acceptable for E2E tests, these can cause flakiness in slower CI environments. Consider using waitForSelector or expect(...).toBeVisible() with appropriate timeouts instead where feasible.

Also applies to: 129-129, 184-184, 192-192, 216-216, 222-222, 300-300, 362-362, 369-369, 390-390, 531-531, 559-559, 564-564

hooks/board/useNoteModal.ts (1)

121-132: Consider adding error handling in the save function.

The save function calls upsertProjectInfo but doesn't handle potential errors. If the save fails, the caller (NoteModal) won't know unless it catches the propagated exception.

Looking at the NoteModal implementation from the relevant_code_snippets, it appears NoteModal handles errors with try/catch, so this is likely fine. However, you may want to add Sentry logging here for consistency with open().

♻️ Optional: Add error logging
   const save = useCallback(
     async (note: string, links: ProjectLink[]) => {
       if (!cardId) return
 
+      try {
         await upsertProjectInfo(cardId, {
           note,
           comment: '',
           links,
         })
+      } catch (error) {
+        Sentry.captureException(error, { tags: { action: 'saveProjectInfo' } })
+        throw error // Re-throw so caller can handle
+      }
     },
     [cardId],
   )

Address CodeRabbit review comments for PR #39:

1. MaintenanceClient.tsx: Add activeNoteRepoId ref to guard async
   getMaintenanceProjectInfo calls. If user switches repos before
   the promise resolves, the stale data is discarded.

2. NoteModal.tsx: Add didInitRef to prevent useEffect from resetting
   editingUrlIndex and deletedLink on every draft update. Now only
   initializes when modal opens, not on each keystroke.
The CI workflow expected coverage reports at `coverage-e2e-shard-N/`
but Playwright outputs to `coverage-e2e/`. Fixed by:

1. Changed upload path from `coverage-e2e-shard-${{ matrix.shardIndex }}/`
   to `coverage-e2e/` to match actual Playwright output

2. Added defensive check for missing `all-coverage-reports` directory
   in merge step to prevent ENOENT errors
@github-actions
Copy link

🧪 E2E Coverage Report (Sharded: 12 parallel jobs)

Metric Coverage
Lines 91.34%
Functions 16.77%
Branches 15.81%
Statements 28.34%

📊 Full report available in workflow artifacts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.github/workflows/e2e.yml:
- Around line 122-127: The workflow currently only checks for the existence of
the all-coverage-reports directory, but actions/download-artifact may create an
empty directory; update the guard so the job treats an empty directory the same
as a missing one by also checking for the presence of any coverage summary files
(e.g., test shard summaries) inside all-coverage-reports and writing the zeroed
JSON to coverage-e2e/coverage/coverage-summary.json when none are found; apply
the same change for the later identical block (lines referenced in the comment)
so both places use a combined "directory exists AND contains files" condition
rather than just -d, and remove the redundant Node.js guard mentioned in the
comment.

Comment on lines +122 to +127
# Check if coverage reports were downloaded
if [ ! -d "all-coverage-reports" ]; then
echo "No coverage reports downloaded - coverage collection may have failed in shards"
echo '{"total":{"lines":{"total":0,"covered":0,"pct":0},"statements":{"total":0,"covered":0,"pct":0},"functions":{"total":0,"covered":0,"pct":0},"branches":{"total":0,"covered":0,"pct":0}}}' > coverage-e2e/coverage/coverage-summary.json
exit 0
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle empty all-coverage-reports to keep summary deterministic.
actions/download-artifact can create the directory even when no artifacts match. In that case the current check won’t fire, and no zeroed summary is produced, so later steps won’t post coverage. Consider treating “no summaries found” the same as missing dir, and drop the redundant Node guard.

🔧 Suggested guard update
-          if [ ! -d "all-coverage-reports" ]; then
+          if [ ! -d "all-coverage-reports" ] || ! find all-coverage-reports -type f -name 'coverage-summary.json' -print -quit | grep -q .; then
             echo "No coverage reports downloaded - coverage collection may have failed in shards"
             echo '{"total":{"lines":{"total":0,"covered":0,"pct":0},"statements":{"total":0,"covered":0,"pct":0},"functions":{"total":0,"covered":0,"pct":0},"branches":{"total":0,"covered":0,"pct":0}}}' > coverage-e2e/coverage/coverage-summary.json
             exit 0
           fi
-            if (!fs.existsSync('all-coverage-reports')) {
-              console.log('No coverage reports directory found');
-              process.exit(0);
-            }
+            // (Optional) remove this guard since the bash check already handles missing/empty coverage.

Also applies to: 148-151

🤖 Prompt for AI Agents
In @.github/workflows/e2e.yml around lines 122 - 127, The workflow currently
only checks for the existence of the all-coverage-reports directory, but
actions/download-artifact may create an empty directory; update the guard so the
job treats an empty directory the same as a missing one by also checking for the
presence of any coverage summary files (e.g., test shard summaries) inside
all-coverage-reports and writing the zeroed JSON to
coverage-e2e/coverage/coverage-summary.json when none are found; apply the same
change for the later identical block (lines referenced in the comment) so both
places use a combined "directory exists AND contains files" condition rather
than just -d, and remove the redundant Node.js guard mentioned in the comment.

@ryota-murakami ryota-murakami merged commit d040703 into main Jan 27, 2026
20 checks passed
@ryota-murakami ryota-murakami deleted the feat/issue-37-consolidate-note-link-modal branch January 27, 2026 13:51
@ryota-murakami ryota-murakami restored the feat/issue-37-consolidate-note-link-modal branch January 27, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consolidate Note and Link editing into NoteModal

3 participants