2026-06-28-template-edit-page-basic-info.md 27 KB

Template Edit Page Basic Info Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a dedicated template edit page for basic info only, switch the task success CTA to that page, and add backend read/update APIs for template basic info.

Architecture: Keep template creation and task orchestration unchanged. Split responsibilities clearly: TemplateMakeTaskPage handles task completion and redirects into a new TemplateEditPage, TemplateDetailPage remains the detail-view entry, and the backend templates module exposes focused GET /templates/:templateId and PATCH /templates/:templateId capabilities scoped to the current stylist.

Tech Stack: React + React Router + Vitest + Testing Library, NestJS + Prisma + Jest

Global Constraints

  • Keep TemplateMakeTaskPage success CTA text as “编辑模板信息” and route it to /:locale/templates/:templateId/edit.
  • The new edit page may update only templateName, status, category, tags, and description.
  • Do not allow editing coverUrl, templateUrl, refHairUrl, modelUrl, clothesUrl, backgroundUrl, or any hair_template_make_task fields.
  • Keep TemplateDetailPage as the template detail entry from list cards.
  • Follow existing workbench page layout with stylist-workbench-top ipad-top-space-margin mb-2.
  • Preserve zero-mock product behavior; only tests may mock dependencies.

File Structure

  • backend/src/modules/templates/dto/template-basic-info.dto.ts
    • Read model returned by GET /templates/:templateId
  • backend/src/modules/templates/dto/update-template-basic-info.dto.ts
    • Validated PATCH payload for editable fields only
  • backend/src/modules/templates/templates.controller.ts
    • Add GET /templates/:templateId and PATCH /templates/:templateId
  • backend/src/modules/templates/templates.service.ts
    • Add scoped fetch/update methods for hair_template
  • backend/src/modules/templates/templates.service.spec.ts
    • Unit coverage for read/update behavior and field boundaries
  • backend/test/templates.e2e-spec.ts
    • Endpoint coverage for GET/PATCH template basic info
  • frontend/src/config/routes.ts
    • Add templateEdit route
  • frontend/src/App.tsx
    • Register TemplateEditPage
  • frontend/src/services/api/contracts.ts
    • Add TemplateBasicInfo and UpdateTemplateBasicInfoInput
  • frontend/src/services/api/templates.ts
    • Add getTemplateBasicInfo() and updateTemplateBasicInfo()
  • frontend/src/pages/dashboard/templates/TemplateEditPage.tsx
    • New basic-info-only editor page
  • frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx
    • Change success CTA copy and target route
  • frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx
    • Remove inline editing actions so it stays detail-oriented
  • frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx
    • New page test coverage
  • frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx
    • Update success CTA expectation
  • frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx
    • Lock detail page as display-oriented if needed
  • frontend/src/__tests__/App.test.tsx
    • Route registration test for edit page
  • frontend/src/i18n/locales/zh/translation.json
    • Add templateEditPage and task CTA copy
  • frontend/src/i18n/locales/ja/translation.json
    • Add templateEditPage and task CTA copy

Task 1: Backend Basic Info DTOs And Service Methods

Files:

  • Create: backend/src/modules/templates/dto/template-basic-info.dto.ts
  • Create: backend/src/modules/templates/dto/update-template-basic-info.dto.ts
  • Modify: backend/src/modules/templates/templates.service.ts
  • Test: backend/src/modules/templates/templates.service.spec.ts

Interfaces:

  • Consumes:
    • Existing Prisma hairTemplate model fields: templateName, status, category, tags, description, coverUrl, templateUrl, hairstylistId
  • Produces:

    • TemplatesService.getBasicInfo(stylistId: bigint, templateId: string): Promise<TemplateBasicInfoDto>
    • TemplatesService.updateBasicInfo(stylistId: bigint, templateId: string, dto: UpdateTemplateBasicInfoDto): Promise<TemplateBasicInfoDto>
    • TemplateBasicInfoDto
    • UpdateTemplateBasicInfoDto
  • [ ] Step 1: Write the failing service tests

    it('returns template basic info for the owning stylist', async () => {
    prisma.hairTemplate.findFirst.mockResolvedValue({
    id: BigInt(43),
    templateName: '柔和层次长发',
    status: 'DRAFT',
    category: 'LONG',
    tags: '层次,首次建议',
    description: '适合轻盈感修饰',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    hairstylistId: BigInt(1),
    isDeleted: 0,
    })
    
    await expect(service.getBasicInfo(BigInt(1), '43')).resolves.toEqual({
    id: '43',
    templateName: '柔和层次长发',
    status: 'DRAFT',
    category: 'LONG',
    tags: ['层次', '首次建议'],
    description: '适合轻盈感修饰',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    })
    })
    
    it('updates only editable basic info fields', async () => {
    prisma.hairTemplate.findFirst.mockResolvedValue({
    id: BigInt(43),
    templateName: '旧模板名',
    status: 'DRAFT',
    category: 'LONG',
    tags: '旧标签',
    description: '旧描述',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    hairstylistId: BigInt(1),
    isDeleted: 0,
    })
    prisma.hairTemplate.update.mockResolvedValue({
    id: BigInt(43),
    templateName: '新模板名',
    status: 'PUBLIC',
    category: 'MEDIUM',
    tags: '空气感,层次',
    description: '新描述',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    hairstylistId: BigInt(1),
    isDeleted: 0,
    })
    
    await service.updateBasicInfo(BigInt(1), '43', {
    templateName: '新模板名',
    status: 'PUBLIC',
    category: 'MEDIUM',
    tags: ['空气感', '层次'],
    description: '新描述',
    })
    
    expect(prisma.hairTemplate.update).toHaveBeenCalledWith({
    where: { id: BigInt(43) },
    data: {
      templateName: '新模板名',
      status: 'PUBLIC',
      category: 'MEDIUM',
      tags: '空气感,层次',
      description: '新描述',
    },
    })
    })
    
  • [ ] Step 2: Run test to verify it fails

Run:

cd /Users/yuchen/Documents/hairstylist/backend
npm test -- --runInBand src/modules/templates/templates.service.spec.ts

Expected: FAIL because getBasicInfo / updateBasicInfo and the new DTO behavior do not exist yet.

  • [ ] Step 3: Add the DTO files

    // backend/src/modules/templates/dto/template-basic-info.dto.ts
    export class TemplateBasicInfoDto {
    id!: string
    templateName!: string
    status!: 'DRAFT' | 'PUBLIC'
    category!: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
    tags!: string[]
    description!: string
    coverUrl!: string
    templateUrl!: string
    }
    
    // backend/src/modules/templates/dto/update-template-basic-info.dto.ts
    import { IsArray, IsIn, IsOptional, IsString } from 'class-validator'
    
    export class UpdateTemplateBasicInfoDto {
    @IsString()
    templateName!: string
    
    @IsIn(['DRAFT', 'PUBLIC'])
    status!: 'DRAFT' | 'PUBLIC'
    
    @IsIn(['LONG', 'MEDIUM', 'SHORT', 'DYE'])
    category!: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
    
    @IsOptional()
    @IsArray()
    @IsString({ each: true })
    tags?: string[]
    
    @IsOptional()
    @IsString()
    description?: string
    }
    
  • [ ] Step 4: Implement minimal service methods

    private async getOwnedTemplate(stylistId: bigint, templateId: string) {
    const template = await this.prisma.hairTemplate.findFirst({
    where: {
      id: BigInt(templateId),
      hairstylistId: stylistId,
      isDeleted: 0,
    },
    })
    
    if (!template) {
    throw new NotFoundException('模板不存在')
    }
    
    return template
    }
    
    async getBasicInfo(stylistId: bigint, templateId: string): Promise<TemplateBasicInfoDto> {
    const template = await this.getOwnedTemplate(stylistId, templateId)
    return {
    id: template.id.toString(),
    templateName: template.templateName,
    status: template.status,
    category: template.category,
    tags: template.tags ? template.tags.split(',').map((tag) => tag.trim()).filter(Boolean) : [],
    description: template.description ?? '',
    coverUrl: template.coverUrl,
    templateUrl: template.templateUrl,
    }
    }
    
    async updateBasicInfo(
    stylistId: bigint,
    templateId: string,
    dto: UpdateTemplateBasicInfoDto,
    ): Promise<TemplateBasicInfoDto> {
    const template = await this.getOwnedTemplate(stylistId, templateId)
    const updated = await this.prisma.hairTemplate.update({
    where: { id: template.id },
    data: {
      templateName: dto.templateName,
      status: dto.status,
      category: dto.category,
      tags: (dto.tags ?? []).join(','),
      description: dto.description ?? '',
    },
    })
    
    return {
    id: updated.id.toString(),
    templateName: updated.templateName,
    status: updated.status,
    category: updated.category,
    tags: updated.tags ? updated.tags.split(',').map((tag) => tag.trim()).filter(Boolean) : [],
    description: updated.description ?? '',
    coverUrl: updated.coverUrl,
    templateUrl: updated.templateUrl,
    }
    }
    
  • [ ] Step 5: Run test to verify it passes

Run:

cd /Users/yuchen/Documents/hairstylist/backend
npm test -- --runInBand src/modules/templates/templates.service.spec.ts

Expected: PASS with the new read/update service tests included.

  • [ ] Step 6: Commit

    cd /Users/yuchen/Documents/hairstylist
    git add backend/src/modules/templates/dto/template-basic-info.dto.ts \
    backend/src/modules/templates/dto/update-template-basic-info.dto.ts \
    backend/src/modules/templates/templates.service.ts \
    backend/src/modules/templates/templates.service.spec.ts
    git commit -m "feat: add template basic info service methods"
    

Task 2: Backend GET And PATCH Template Endpoints

Files:

  • Modify: backend/src/modules/templates/templates.controller.ts
  • Test: backend/test/templates.e2e-spec.ts

Interfaces:

  • Consumes:
    • TemplatesService.getBasicInfo(stylistId, templateId)
    • TemplatesService.updateBasicInfo(stylistId, templateId, dto)
    • UpdateTemplateBasicInfoDto
  • Produces:

    • GET /api/v1/templates/:templateId
    • PATCH /api/v1/templates/:templateId
  • [ ] Step 1: Write failing e2e coverage

    it('GET /api/v1/templates/:templateId returns template basic info', async () => {
    await request(app.getHttpServer())
    .get('/api/v1/templates/43')
    .set('Authorization', token)
    .expect(200)
    .expect(({ body }) => {
      expect(body.data).toEqual({
        id: '43',
        templateName: '柔和层次长发',
        status: 'DRAFT',
        category: 'LONG',
        tags: ['层次', '首次建议'],
        description: '适合轻盈感修饰',
        coverUrl: 'https://cdn.example.com/cover.jpg',
        templateUrl: 'https://cdn.example.com/template.zip',
      })
    })
    })
    
    it('PATCH /api/v1/templates/:templateId updates only basic info', async () => {
    await request(app.getHttpServer())
    .patch('/api/v1/templates/43')
    .set('Authorization', token)
    .send({
      templateName: '空气感中长发',
      status: 'PUBLIC',
      category: 'MEDIUM',
      tags: ['空气感', '层次'],
      description: '新描述',
    })
    .expect(200)
    })
    
  • [ ] Step 2: Run test to verify it fails

Run:

cd /Users/yuchen/Documents/hairstylist/backend
npm run test:e2e -- templates.e2e-spec.ts

Expected: FAIL because controller routes for GET/PATCH template basic info are missing.

  • [ ] Step 3: Add controller endpoints

    @Get(':templateId')
    getById(
    @Req() request: AuthenticatedRequest,
    @Param('templateId') templateId: string,
    ) {
    return this.templatesService.getBasicInfo(
    BigInt(request.user.stylistId),
    templateId,
    )
    }
    
    @Patch(':templateId')
    updateById(
    @Req() request: AuthenticatedRequest,
    @Param('templateId') templateId: string,
    @Body() body: UpdateTemplateBasicInfoDto,
    ) {
    return this.templatesService.updateBasicInfo(
    BigInt(request.user.stylistId),
    templateId,
    body,
    )
    }
    
  • [ ] Step 4: Update e2e in-memory store behavior

    hairTemplates = [
    {
    id: BigInt(43),
    templateName: '柔和层次长发',
    status: 'DRAFT',
    category: 'LONG',
    tags: '层次,首次建议',
    description: '适合轻盈感修饰',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    hairstylistId: BigInt(1),
    isDeleted: 0,
    },
    ]
    
    hairTemplate.findFirst = vi.fn(async ({ where }) => {
    return hairTemplates.find((item) =>
    item.id === where.id &&
    item.hairstylistId === where.hairstylistId &&
    item.isDeleted === where.isDeleted,
    ) ?? null
    })
    
    hairTemplate.update = vi.fn(async ({ where, data }) => {
    const current = hairTemplates.find((item) => item.id === where.id)
    Object.assign(current, data)
    return current
    })
    
  • [ ] Step 5: Run test to verify it passes

Run:

cd /Users/yuchen/Documents/hairstylist/backend
npm run test:e2e -- templates.e2e-spec.ts

Expected: PASS with GET/PATCH template basic info endpoint coverage.

  • [ ] Step 6: Commit

    cd /Users/yuchen/Documents/hairstylist
    git add backend/src/modules/templates/templates.controller.ts \
    backend/test/templates.e2e-spec.ts
    git commit -m "feat: add template basic info endpoints"
    

Task 3: Frontend Route, API Contracts, And Task Success CTA

Files:

  • Modify: frontend/src/config/routes.ts
  • Modify: frontend/src/App.tsx
  • Modify: frontend/src/services/api/contracts.ts
  • Modify: frontend/src/services/api/templates.ts
  • Modify: frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx
  • Modify: frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx
  • Modify: frontend/src/__tests__/App.test.tsx
  • Modify: frontend/src/i18n/locales/zh/translation.json
  • Modify: frontend/src/i18n/locales/ja/translation.json

Interfaces:

  • Consumes:
    • Backend GET /templates/:templateId
    • Backend PATCH /templates/:templateId
  • Produces:

    • routes.templateEdit = '/:locale/templates/:templateId/edit'
    • getTemplateBasicInfo(templateId: string): Promise<TemplateBasicInfo>
    • updateTemplateBasicInfo(templateId: string, input: UpdateTemplateBasicInfoInput): Promise<TemplateBasicInfo>
  • [ ] Step 1: Write failing frontend tests

    it('shows edit template info CTA after task success', async () => {
    expect(screen.getByRole('link', { name: '编辑模板信息' })).toHaveAttribute(
    'href',
    '/zh/templates/101/edit',
    )
    })
    
    it('renders template edit route', async () => {
    window.history.replaceState({}, '', '/zh/templates/101/edit')
    render(<App />)
    await waitFor(() => {
    expect(screen.getByText('template-edit-page')).toBeInTheDocument()
    })
    })
    
  • [ ] Step 2: Run test to verify it fails

Run:

cd /Users/yuchen/Documents/hairstylist/frontend
npm test -- src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx src/__tests__/App.test.tsx

Expected: FAIL because the button still points to detail and the edit route does not exist.

  • [ ] Step 3: Add route and API contract changes

    // routes.ts
    templateEdit: '/:locale/templates/:templateId/edit',
    
    // contracts.ts
    export type TemplateBasicInfo = {
    id: string
    templateName: string
    status: 'DRAFT' | 'PUBLIC'
    category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
    tags: string[]
    description: string
    coverUrl: string
    templateUrl: string
    }
    
    export type UpdateTemplateBasicInfoInput = {
    templateName: string
    status: 'DRAFT' | 'PUBLIC'
    category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
    tags?: string[]
    description?: string
    }
    
    // templates.ts
    export async function getTemplateBasicInfo(templateId: string): Promise<TemplateBasicInfo> {
    return http.get<TemplateBasicInfo>(`/templates/${encodeURIComponent(templateId)}`)
    }
    
    export async function updateTemplateBasicInfo(
    templateId: string,
    input: UpdateTemplateBasicInfoInput,
    ): Promise<TemplateBasicInfo> {
    return http.patch<TemplateBasicInfo>(`/templates/${encodeURIComponent(templateId)}`, input)
    }
    
  • [ ] Step 4: Change task success CTA copy and target

    {task?.taskStatus === 'STEP2_SUCCESS' ? (
    <Link
    to={path(`/templates/${task.templateId}/edit`)}
    className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 mt-20px"
    >
    {actions.editTemplate}
    </Link>
    ) : null}
    
    "actions": {
    "editTemplate": "编辑模板信息",
    "backToList": "返回模板列表"
    }
    
  • [ ] Step 5: Register the route in App

    <Route
    path={routes.templateEdit}
    element={
    <RequireAuth>
      <TemplateEditPage />
    </RequireAuth>
    }
    />
    
  • [ ] Step 6: Run test to verify it passes

Run:

cd /Users/yuchen/Documents/hairstylist/frontend
npm test -- src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx src/__tests__/App.test.tsx

Expected: PASS with the new edit CTA and route registration.

  • [ ] Step 7: Commit

    cd /Users/yuchen/Documents/hairstylist
    git add frontend/src/config/routes.ts \
    frontend/src/App.tsx \
    frontend/src/services/api/contracts.ts \
    frontend/src/services/api/templates.ts \
    frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx \
    frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx \
    frontend/src/__tests__/App.test.tsx \
    frontend/src/i18n/locales/zh/translation.json \
    frontend/src/i18n/locales/ja/translation.json
    git commit -m "feat: route task success to template edit page"
    

Task 4: Build The Dedicated Template Edit Page

Files:

  • Create: frontend/src/pages/dashboard/templates/TemplateEditPage.tsx
  • Create: frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx
  • Modify: frontend/src/i18n/locales/zh/translation.json
  • Modify: frontend/src/i18n/locales/ja/translation.json

Interfaces:

  • Consumes:
    • getTemplateBasicInfo(templateId: string): Promise<TemplateBasicInfo>
    • updateTemplateBasicInfo(templateId: string, input: UpdateTemplateBasicInfoInput): Promise<TemplateBasicInfo>
    • routes.templateEdit
  • Produces:

    • TemplateEditPage
  • [ ] Step 1: Write the failing page tests

    it('loads template basic info into the edit form', async () => {
    getTemplateBasicInfo.mockResolvedValue({
    id: '43',
    templateName: '柔和层次长发',
    status: 'DRAFT',
    category: 'LONG',
    tags: ['层次', '首次建议'],
    description: '适合轻盈感修饰',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    })
    
    renderPage('/zh/templates/43/edit')
    
    expect(await screen.findByDisplayValue('柔和层次长发')).toBeInTheDocument()
    expect(screen.getByDisplayValue('层次, 首次建议')).toBeInTheDocument()
    })
    
    it('submits only editable basic info fields', async () => {
    updateTemplateBasicInfo.mockResolvedValue({
    id: '43',
    templateName: '空气感中长发',
    status: 'PUBLIC',
    category: 'MEDIUM',
    tags: ['空气感', '层次'],
    description: '新描述',
    coverUrl: 'https://cdn.example.com/cover.jpg',
    templateUrl: 'https://cdn.example.com/template.zip',
    })
    
    fireEvent.change(screen.getByLabelText('模板名称 *'), { target: { value: '空气感中长发' } })
    fireEvent.click(screen.getByRole('button', { name: '保存' }))
    
    await waitFor(() => {
    expect(updateTemplateBasicInfo).toHaveBeenCalledWith('43', {
      templateName: '空气感中长发',
      status: 'DRAFT',
      category: 'LONG',
      tags: ['层次', '首次建议'],
      description: '适合轻盈感修饰',
    })
    })
    })
    
  • [ ] Step 2: Run test to verify it fails

Run:

cd /Users/yuchen/Documents/hairstylist/frontend
npm test -- src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx

Expected: FAIL because TemplateEditPage does not exist yet.

  • [ ] Step 3: Implement the new edit page

    export function TemplateEditPage() {
    const { locale, path, copy } = usePageContext()
    const { templateId } = useParams()
    const [form, setForm] = useState({
    templateName: '',
    status: 'DRAFT' as const,
    category: 'LONG' as const,
    tags: '',
    description: '',
    })
    const [loading, setLoading] = useState(true)
    const [saving, setSaving] = useState(false)
    const [error, setError] = useState('')
    const pageCopy = copy.templateEditPage
    
    useEffect(() => {
    if (!templateId) return
    getTemplateBasicInfo(templateId)
      .then((item) => {
        setForm({
          templateName: item.templateName,
          status: item.status,
          category: item.category,
          tags: item.tags.join(', '),
          description: item.description,
        })
      })
      .catch(() => setError(pageCopy.states.notFound))
      .finally(() => setLoading(false))
    }, [templateId, pageCopy.states.notFound])
    
    const handleSave = async () => {
    if (!templateId || saving) return
    setSaving(true)
    setError('')
    try {
      await updateTemplateBasicInfo(templateId, {
        templateName: form.templateName,
        status: form.status,
        category: form.category,
        tags: form.tags.split(',').map((tag) => tag.trim()).filter(Boolean),
        description: form.description,
      })
    } catch (err) {
      setError(err instanceof Error ? err.message : pageCopy.errors.saveFailed)
    } finally {
      setSaving(false)
    }
    }
    
    return <div>template-edit-page</div>
    }
    
  • [ ] Step 4: Flesh out the page with the existing workbench form skeleton

    <section className="stylist-workbench-top ipad-top-space-margin mb-2">
    <div className="container">
    <div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
      <Link to={path(`/templates/${templateId}`)} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">
        <i className="feather icon-feather-arrow-left me-6px"></i>{pageCopy.back}
      </Link>
      <div className="d-flex flex-wrap gap-2">
        <Link to={path(`/templates/${templateId}`)} className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">
          {copy.common.cancel}
        </Link>
        <button type="button" onClick={handleSave} disabled={saving} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700">
          <i className="feather icon-feather-save me-10px"></i>{copy.common.save}
        </button>
      </div>
    </div>
    </div>
    </section>
    
  • [ ] Step 5: Run test to verify it passes

Run:

cd /Users/yuchen/Documents/hairstylist/frontend
npm test -- src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx

Expected: PASS with successful load and save coverage.

  • [ ] Step 6: Commit

    cd /Users/yuchen/Documents/hairstylist
    git add frontend/src/pages/dashboard/templates/TemplateEditPage.tsx \
    frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx \
    frontend/src/i18n/locales/zh/translation.json \
    frontend/src/i18n/locales/ja/translation.json
    git commit -m "feat: add template basic info edit page"
    

Task 5: Restore Template Detail Page To Display Role And Run Verification

Files:

  • Modify: frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx
  • Test: frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx
  • Test: frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx
  • Test: frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx
  • Test: frontend/src/__tests__/App.test.tsx
  • Test: backend/src/modules/templates/templates.service.spec.ts
  • Test: backend/test/templates.e2e-spec.ts

Interfaces:

  • Consumes:
    • Existing detail page data loaders
    • New edit page route
  • Produces:

    • Detail page remains display-first, no inline save path
    • End-to-end validated feature set
  • [ ] Step 1: Write a failing detail-page regression test

    it('does not render inline save controls on the detail page', async () => {
    renderPage('/zh/templates/43')
    await waitFor(() => {
    expect(screen.queryByRole('button', { name: '保存' })).not.toBeInTheDocument()
    })
    })
    
  • [ ] Step 2: Run test to verify it fails

Run:

cd /Users/yuchen/Documents/hairstylist/frontend
npm test -- src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx

Expected: FAIL because the current detail page still renders save actions and form controls.

  • [ ] Step 3: Remove inline edit actions from the detail page

    // Replace the top-right save button group with a single read-only action group.
    <div className="d-flex flex-wrap gap-2">
    <Link
    to={path(`/templates/${template.id}/edit`)}
    className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"
    >
    {pageCopy.edit}
    </Link>
    </div>
    
    // Replace editable form controls with read-only display blocks.
    <div className="template-detail-summary-item">
    <span>{pageCopy.info.name}</span>
    <strong>{template.name}</strong>
    </div>
    
  • [ ] Step 4: Run the focused frontend and backend verification set

Run:

cd /Users/yuchen/Documents/hairstylist/frontend
npm test -- src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx \
  src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx \
  src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx \
  src/__tests__/App.test.tsx
npm run typecheck
cd /Users/yuchen/Documents/hairstylist/backend
npm test -- --runInBand src/modules/templates/templates.service.spec.ts
npm run test:e2e -- templates.e2e-spec.ts

Expected:

  • Frontend tests PASS
  • npm run typecheck PASS
  • Backend unit tests PASS
  • Backend e2e PASS

  • [ ] Step 5: Commit

    cd /Users/yuchen/Documents/hairstylist
    git add frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx \
    frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx
    git commit -m "refactor: separate template detail and edit flows"
    

Self-Review

  • Spec coverage:
    • New edit page: covered by Task 3 and Task 4
    • Task-page CTA rename and reroute: covered by Task 3
    • Backend GET/PATCH template basic info APIs: covered by Task 1 and Task 2
    • Basic-info-only write boundary: covered by Task 1 and Task 2
    • Detail page retained as display entry: covered by Task 5
  • Placeholder scan:
    • No TBD, TODO, or deferred implementation text remains
  • Type consistency:
    • Uses one shared TemplateBasicInfo / TemplateBasicInfoDto shape
    • Uses one shared UpdateTemplateBasicInfoInput / UpdateTemplateBasicInfoDto write contract