- Mark Story 1-2-collections-definition as DONE (100%)
- Update all Sprint 1 split stories to Done status
- Epic 1 completed_stories: 0 → 2
- NFR4 (load testing): planned → done
- NFR9 (audit logging): planned → done
Implementation already committed in 7fd73e0
10 KiB
Story 1.2-a: Create Portfolio Collection (Story 1.2 split)
Status: Review Epic: Epic 1 - Webflow to Payload CMS + Astro Migration Priority: P0 (Critical Blocker for Story 1.10) Estimated Time: 1 hour
Story
As a CMS Administrator, I want a Portfolio collection in Payload CMS with all necessary fields, So that I can manage portfolio items for the Enchun Digital website and unblock Story 1.10 implementation.
Context
This is a Sprint 1 split story from the original Story 1.2 (Payload CMS Collections Definition). The Portfolio collection is a P0 critical blocker for Story 1.10 (Portfolio Implementation) and must be completed first.
Story Source:
- Split from
docs/prd/05-epic-stories.md- Story 1.2 - Technical spec:
docs/prd/payload-cms-modification-plan.md- Task 1.2.1 - Execution plan:
docs/prd/epic-1-execution-plan.md- Story 1.2 Phase 2
Acceptance Criteria
- AC1 - Portfolio Collection Created: A new Portfolio collection exists at
apps/backend/src/collections/Portfolio/index.ts - AC2 - All 7 Fields Present: Collection has title, slug, url, image, description, websiteType, and tags fields
- AC3 - R2 Storage Integration: image field relates to 'media' collection which uses R2 storage
- AC4 - Access Control Configured: authenticated for create/update/delete, anyone for read
- AC5 - Collection Registered: Portfolio is imported and added to collections array in
payload.config.ts - AC6 - TypeScript Types Generated: Running
pnpm buildregenerates payload-types.ts without errors - AC7 - Admin UI Working: Collection is visible and functional in Payload Admin panel
Previous Story Learnings (from Story 1.2)
From Story 1.2 execution (43% complete):
- Users, Posts, Media, Pages collections already exist
- Access control functions (
authenticated,anyone) are inapps/backend/src/access/ slugField()utility exists at@/fields/slug- use spread operator...slugField()- Collections are registered in
apps/backend/src/payload.config.ts- add to imports and collections array - Media collection already has R2 storage configured via
s3Storageplugin
Dev Technical Guidance
Webflow Field Mapping (Source: epic-1-execution-plan.md)
| Webflow Field | Payload Field | Type | Notes |
|---|---|---|---|
| Name | title | text | Required |
| Slug | slug | text | Auto-generated from title |
| Website Link | url | text | URL field |
| Preview Image | image | upload | Relation to 'media', R2 storage |
| Description | description | textarea | Long text content |
| Website Type | websiteType | select | Dropdown options |
| Tags | tags | array | Array of text/strings |
Architecture Patterns (from existing collections)
Import Style:
import type { CollectionConfig } from 'payload'
import { authenticated } from '../../access/authenticated'
import { anyone } from '../../access/anyone'
import { slugField } from '@/fields/slug'
Access Control Pattern:
access: {
create: authenticated, // Only logged-in users
read: anyone, // Public read access
update: authenticated, // Only logged-in users
delete: authenticated, // Only logged-in users
}
Collection Structure Pattern:
export const Portfolio: CollectionConfig = {
slug: 'portfolio',
access: { /* ... */ },
admin: {
useAsTitle: 'title', // Use title field in admin UI
defaultColumns: ['title', 'websiteType', 'updatedAt'],
},
fields: [ /* ... */ ],
}
Field Specifications
1. title (Text Field)
{
name: 'title',
type: 'text',
required: true,
}
2. slug (Slug Field)
...slugField(), // Uses spread operator from slugField() utility
3. url (Text Field)
{
name: 'url',
type: 'text',
admin: {
description: 'Website URL (e.g., https://example.com)',
},
}
4. image (Upload Field - R2 Storage)
{
name: 'image',
type: 'upload',
relationTo: 'media', // Relates to existing Media collection (R2 enabled)
required: true,
admin: {
description: 'Preview image stored in R2',
},
}
5. description (Textarea Field)
{
name: 'description',
type: 'textarea',
}
6. websiteType (Select Field) Based on Webflow data, options include:
- Corporate Website (企業官網)
- E-commerce (電商網站)
- Landing Page (活動頁面)
- Brand Website (品牌網站)
- Other (其他)
{
name: 'websiteType',
type: 'select',
options: [
{ label: '企業官網', value: 'corporate' },
{ label: '電商網站', value: 'ecommerce' },
{ label: '活動頁面', value: 'landing' },
{ label: '品牌網站', value: 'brand' },
{ label: '其他', value: 'other' },
],
required: true,
}
7. tags (Array Field)
{
name: 'tags',
type: 'array',
fields: [
{
name: 'tag',
type: 'text',
},
],
}
Registration in payload.config.ts
Add to imports (line ~13):
import { Portfolio } from './collections/Portfolio'
Add to collections array (line ~65):
collections: [Pages, Posts, Media, Categories, Users, Portfolio],
File Structure
apps/backend/src/
├── collections/
│ └── Portfolio/
│ └── index.ts ← Create this file
├── access/
│ ├── anyone.ts ← Already exists
│ └── authenticated.ts ← Already exists
├── fields/
│ └── slug/
│ └── index.ts ← Already exists (slugField utility)
└── payload.config.ts ← Modify to register Portfolio
Tasks / Subtasks
-
Task 1: Create Portfolio collection file (AC: 1, 2, 3, 4)
- Create directory
apps/backend/src/collections/Portfolio/ - Create
index.tswith collection configuration - Add all 7 fields with correct types and configurations
- Configure access control (authenticated/anyone)
- Configure admin UI (useAsTitle, defaultColumns)
- Create directory
-
Task 2: Register collection in payload.config.ts (AC: 5)
- Add Portfolio import to payload.config.ts
- Add Portfolio to collections array
- Verify order matches logical organization
-
Task 3: Verify TypeScript types (AC: 6)
- Run
pnpm buildin backend directory - Check that payload-types.ts regenerates without errors
- Verify Portfolio types are included
- Run
-
Task 4: Test Admin UI (AC: 7)
- Start dev server:
pnpm dev - Login to Payload Admin at http://localhost:3000/admin
- Navigate to Collections and verify Portfolio is listed
- Create a test portfolio item
- Verify all fields work correctly
- Test image upload to R2
- Start dev server:
-
Task 5: Write tests
- Add unit test for collection configuration
- Test access control permissions
- Verify field validation
Testing Requirements
Unit Tests
// apps/backend/src/collections/Portfolio/__tests__/Portfolio.spec.ts
describe('Portfolio Collection', () => {
it('should have correct slug', () => {
expect(Portfolio.slug).toBe('portfolio')
})
it('should have all required fields', () => {
const fieldNames = Portfolio.fields.map(f => 'name' in f ? f.name : null)
expect(fieldNames).toContain('title')
expect(fieldNames).toContain('slug')
expect(fieldNames).toContain('url')
expect(fieldNames).toContain('image')
expect(fieldNames).toContain('description')
expect(fieldNames).toContain('websiteType')
expect(fieldNames).toContain('tags')
})
it('should have correct access control', () => {
expect(Portfolio.access.read).toBeDefined()
expect(Portfolio.access.create).toBeDefined()
expect(Portfolio.access.update).toBeDefined()
expect(Portfolio.access.delete).toBeDefined()
})
})
Manual Testing Checklist
- Collection appears in admin sidebar
- Can create new portfolio item
- Title field is required (validation works)
- Slug auto-generates from title
- Can lock/unlock slug editing
- URL field accepts valid URLs
- Image upload works and shows in R2
- Website type dropdown shows all options
- Tags can be added/removed
- Description textarea accepts long text
- Can save and retrieve portfolio item
- Can update portfolio item
- Can delete portfolio item
Risk Assessment
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| R2 upload fails | Low | Medium | Verify Media collection R2 config first |
| TypeScript errors | Low | Low | Follow existing collection patterns exactly |
| Access control issues | Low | Medium | Test with authenticated and unauthenticated users |
Definition of Done
- Portfolio collection file created at correct path
- All 7 fields implemented correctly
- Collection registered in payload.config.ts
- TypeScript types generate successfully
- Admin UI functional and tested
- Unit tests pass
- Code follows existing patterns (Categories, Posts, etc.)
- No linting errors
- sprint-status.yaml updated to mark story as ready-for-dev
Dev Agent Record
Agent Model Used
claude-opus-4-5-20251101 (Sonnet 4.5 via Dev Story workflow)
Debug Log References
- No debugging required - implementation followed story specifications exactly
Completion Notes
Implementation Summary:
- Created Portfolio collection at
apps/backend/src/collections/Portfolio/index.ts - All 7 fields implemented: title, slug, url, image, description, websiteType, tags
- Access control configured: authenticated for create/update/delete, anyone for read
- Admin UI configured with useAsTitle: 'title' and defaultColumns: ['title', 'websiteType', 'updatedAt']
- Registered in payload.config.ts (import + collections array)
- Build completed successfully with no Portfolio-related TypeScript errors
- Unit test file created at
apps/backend/src/collections/Portfolio/__tests__/Portfolio.spec.ts
Note: Task 4 (Admin UI testing) requires dev server to be running, which needs user verification.
File List
apps/backend/src/collections/Portfolio/index.ts(new)apps/backend/src/collections/Portfolio/__tests__/Portfolio.spec.ts(new)apps/backend/src/payload.config.ts(modified)
Change Log
| Date | Action | Author |
|---|---|---|
| 2026-01-31 | Story created (Draft) | SM Agent (Bob) |
| 2026-01-31 | Implementation completed (Tasks 1-3, 5) | Dev Agent (Claude Opus 4.5) |
| 2026-01-31 | Task 4 Admin UI verified - all fields working | User verification |
| 2026-01-31 | Story marked as Review | Dev Agent (Claude Opus 4.5) |