PR: carta/carta-frontend-platform#18584
# 1. Initial prompt
> "Implement the front-end components and flow for Edit company recipients"
> Figma: https://www.figma.com/design/.../RFI-for-PE-LLCs?node-id=3181-126265
# 2. Generate specification with user stories
> /speckit.specify
# 3. Research technical context (existing patterns, API types)
> /speckit.plan # includes research phase
# 4. Generate dependency-ordered tasks
> /speckit.tasks
# 5. Implement following TDD (tests first)
> /speckit.implementOutput: 32 tasks, 4 components, 2 hooks, 15 stories, 56 tests
Speckit analyzed the codebase to resolve technical decisions:
### API Types (RESOLVED)
**Decision**: Use existing `PointOfContact` type from `@carta/api-rfi-data-collection`
**Rationale**: The schema already defines:
PointOfContact: { email, name, title, primary, is_company_designated }
**Alternatives considered**:
- Create new type β Rejected: duplicates existing well-defined schema
**Location**: `apis/investor-relations/rfi-data-collection/client/schema.d.ts`
---
### Modal Pattern (RESOLVED)
**Decision**: Follow existing `EditRequestEmailModal` pattern
**Rationale**: Established pattern in codebase:
- `Modal.Header`, `Modal.Body`, `Modal.Footer` with `enableNewStyles`
- `VStack`, `HStack` for layout
- `Field` wrapper for form inputs
**Reference**: `src/core/components/PeriodView/modals/EditRequestEmailModal.tsx`
---
### API Mutation Pattern (RESOLVED)
**Decision**: Use `useCallback` with `callApi` (not useMutation)
**Rationale**: Existing pattern in `useExportRfiData`:
const exportData = useCallback(async () => {
await callApi(apiRequestBuilder, { firmId });
}, [firmId]);
**Reference**: `src/core/hooks/mutations/useExportRfiData.ts`Three user stories with acceptance criteria:
### User Story 1 - Carta Designated Contacts (P1) π― MVP
**Given** a Carta company with designated contacts is selected,
**When** I click "Edit contacts" from the row actions menu,
**Then** I see a modal with:
- "Send request to:" section with designated contact
- "Primary" and "Designated" badges
- Message: "This contact is designated by the company and cannot be changed."
- "Add recipient" link for CC recipients
### User Story 2 - Investor Managed Contacts (P2)
- Primary recipient is editable via email search
- Can add/remove CC recipients
### User Story 3 - Missing Contact (P3)
- Empty state with inline form to add new contact32 tasks organized by phase. Tasks marked [P] can run simultaneously:
## Phase 2: Foundational
- [x] T005 [P] Create useUpdateRecipients hook βββ¬ββ Parallel
- [x] T006 [P] Create useGetEntityRecipients hook βββ
## Phase 3: User Story 1 - MVP
- [x] T007 [US1] Write tests for RecipientListItem βββ TDD: tests first
- [x] T009 [P] Create RecipientListItem.tsx βββ¬ββ Parallel
- [x] T010 [P] Create RecipientListItem.stories.tsx βββ
## Phase 4: User Story 2
- [x] T016 [P] Create EmailAutocomplete.tsx βββ¬ββ Parallel
- [x] T017 [P] Create EmailAutocomplete.stories.tsx βββ
## Phase 5: User Story 3
- [x] T023 [P] Create AddRecipientForm.tsx βββ¬ββ Parallel
- [x] T024 [P] Create AddRecipientForm.stories.tsx βββ12 parallel opportunities across 32 tasks.
Modal variants for each contact type scenario:
// CartaDesignated - read-only primary contact
export const CartaDesignated: Story = {
args: {
entityName: 'Acme Corp',
contactType: 'carta_designated',
recipients: [
{ name: 'Jane Smith', isDesignated: true, isPrimary: true },
],
},
};
// InvestorManaged - editable primary
export const InvestorManaged: Story = {
args: {
entityName: 'Beta Inc',
contactType: 'investor_managed',
recipients: [{ name: 'Bob Johnson', isDesignated: false, isPrimary: true }],
},
};
// MissingContact - empty state with add form
export const MissingContact: Story = {
args: {
entityName: 'New Company',
contactType: 'missing',
recipients: [],
},
};| Artifact | Count |
|---|---|
| Tasks completed | 26/32 |
| Components | 4 (EditRecipientsModal, RecipientListItem, AddRecipientForm, EmailAutocomplete) |
| Hooks | 2 (useUpdateRecipients, useGetEntityRecipients) |
| Storybook stories | 15 |
| Tests | 56 passing |