Files
website-enchun-mgr/_bmad-output/implementation-artifacts/1-2-portfolio-collection.story.md
pkupuk 8c87d71aa2 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
2026-01-31 17:34:41 +08:00

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

  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:

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.ts with collection configuration
    • Add all 7 fields with correct types and configurations
    • Configure access control (authenticated/anyone)
    • Configure admin UI (useAsTitle, defaultColumns)
  • 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 build in backend directory
    • Check that payload-types.ts regenerates without errors
    • Verify Portfolio types are included
  • 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
  • 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)