docs: update sprint-status and story files after Sprint 1 completion
- 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
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
# 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
|
||||
|
||||
1. **AC1 - Portfolio Collection Created**: A new Portfolio collection exists at `apps/backend/src/collections/Portfolio/index.ts`
|
||||
2. **AC2 - All 7 Fields Present**: Collection has title, slug, url, image, description, websiteType, and tags fields
|
||||
3. **AC3 - R2 Storage Integration**: image field relates to 'media' collection which uses R2 storage
|
||||
4. **AC4 - Access Control Configured**: authenticated for create/update/delete, anyone for read
|
||||
5. **AC5 - Collection Registered**: Portfolio is imported and added to collections array in `payload.config.ts`
|
||||
6. **AC6 - TypeScript Types Generated**: Running `pnpm build` regenerates payload-types.ts without errors
|
||||
7. **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 in `apps/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 `s3Storage` plugin
|
||||
|
||||
## 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:**
|
||||
```typescript
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { authenticated } from '../../access/authenticated'
|
||||
import { anyone } from '../../access/anyone'
|
||||
import { slugField } from '@/fields/slug'
|
||||
```
|
||||
|
||||
**Access Control Pattern:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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)**
|
||||
```typescript
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
}
|
||||
```
|
||||
|
||||
**2. slug (Slug Field)**
|
||||
```typescript
|
||||
...slugField(), // Uses spread operator from slugField() utility
|
||||
```
|
||||
|
||||
**3. url (Text Field)**
|
||||
```typescript
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'Website URL (e.g., https://example.com)',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**4. image (Upload Field - R2 Storage)**
|
||||
```typescript
|
||||
{
|
||||
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)**
|
||||
```typescript
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
}
|
||||
```
|
||||
|
||||
**6. websiteType (Select Field)**
|
||||
Based on Webflow data, options include:
|
||||
- Corporate Website (企業官網)
|
||||
- E-commerce (電商網站)
|
||||
- Landing Page (活動頁面)
|
||||
- Brand Website (品牌網站)
|
||||
- Other (其他)
|
||||
|
||||
```typescript
|
||||
{
|
||||
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)**
|
||||
```typescript
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'array',
|
||||
fields: [
|
||||
{
|
||||
name: 'tag',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Registration in payload.config.ts
|
||||
|
||||
Add to imports (line ~13):
|
||||
```typescript
|
||||
import { Portfolio } from './collections/Portfolio'
|
||||
```
|
||||
|
||||
Add to collections array (line ~65):
|
||||
```typescript
|
||||
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
|
||||
|
||||
- [x] **Task 1: Create Portfolio collection file** (AC: 1, 2, 3, 4)
|
||||
- [x] Create directory `apps/backend/src/collections/Portfolio/`
|
||||
- [x] Create `index.ts` with collection configuration
|
||||
- [x] Add all 7 fields with correct types and configurations
|
||||
- [x] Configure access control (authenticated/anyone)
|
||||
- [x] Configure admin UI (useAsTitle, defaultColumns)
|
||||
|
||||
- [x] **Task 2: Register collection in payload.config.ts** (AC: 5)
|
||||
- [x] Add Portfolio import to payload.config.ts
|
||||
- [x] Add Portfolio to collections array
|
||||
- [x] Verify order matches logical organization
|
||||
|
||||
- [x] **Task 3: Verify TypeScript types** (AC: 6)
|
||||
- [x] Run `pnpm build` in backend directory
|
||||
- [x] Check that payload-types.ts regenerates without errors
|
||||
- [x] Verify Portfolio types are included
|
||||
|
||||
- [x] **Task 4: Test Admin UI** (AC: 7)
|
||||
- [x] Start dev server: `pnpm dev`
|
||||
- [x] Login to Payload Admin at http://localhost:3000/admin
|
||||
- [x] Navigate to Collections and verify Portfolio is listed
|
||||
- [x] Create a test portfolio item
|
||||
- [x] Verify all fields work correctly
|
||||
- [x] Test image upload to R2
|
||||
|
||||
- [x] **Task 5: Write tests**
|
||||
- [x] Add unit test for collection configuration
|
||||
- [x] Test access control permissions
|
||||
- [x] Verify field validation
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
```typescript
|
||||
// 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) |
|
||||
Reference in New Issue
Block a user