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
TemplateMakeTaskPage success CTA text as “编辑模板信息” and route it to /:locale/templates/:templateId/edit.templateName, status, category, tags, and description.coverUrl, templateUrl, refHairUrl, modelUrl, clothesUrl, backgroundUrl, or any hair_template_make_task fields.TemplateDetailPage as the template detail entry from list cards.stylist-workbench-top ipad-top-space-margin mb-2.backend/src/modules/templates/dto/template-basic-info.dto.ts
GET /templates/:templateIdbackend/src/modules/templates/dto/update-template-basic-info.dto.ts
backend/src/modules/templates/templates.controller.ts
GET /templates/:templateId and PATCH /templates/:templateIdbackend/src/modules/templates/templates.service.ts
hair_templatebackend/src/modules/templates/templates.service.spec.ts
backend/test/templates.e2e-spec.ts
frontend/src/config/routes.ts
templateEdit routefrontend/src/App.tsx
TemplateEditPagefrontend/src/services/api/contracts.ts
TemplateBasicInfo and UpdateTemplateBasicInfoInputfrontend/src/services/api/templates.ts
getTemplateBasicInfo() and updateTemplateBasicInfo()frontend/src/pages/dashboard/templates/TemplateEditPage.tsx
frontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsx
frontend/src/pages/dashboard/templates/TemplateDetailPage.tsx
frontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsx
frontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsx
frontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsx
frontend/src/__tests__/App.test.tsx
frontend/src/i18n/locales/zh/translation.json
templateEditPage and task CTA copyfrontend/src/i18n/locales/ja/translation.json
templateEditPage and task CTA copyFiles:
backend/src/modules/templates/dto/template-basic-info.dto.tsbackend/src/modules/templates/dto/update-template-basic-info.dto.tsbackend/src/modules/templates/templates.service.tsbackend/src/modules/templates/templates.service.spec.tsInterfaces:
hairTemplate model fields: templateName, status, category, tags, description, coverUrl, templateUrl, hairstylistIdProduces:
TemplatesService.getBasicInfo(stylistId: bigint, templateId: string): Promise<TemplateBasicInfoDto>TemplatesService.updateBasicInfo(stylistId: bigint, templateId: string, dto: UpdateTemplateBasicInfoDto): Promise<TemplateBasicInfoDto>TemplateBasicInfoDtoUpdateTemplateBasicInfoDto[ ] 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"
Files:
backend/src/modules/templates/templates.controller.tsbackend/test/templates.e2e-spec.tsInterfaces:
TemplatesService.getBasicInfo(stylistId, templateId)TemplatesService.updateBasicInfo(stylistId, templateId, dto)UpdateTemplateBasicInfoDtoProduces:
GET /api/v1/templates/:templateIdPATCH /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"
Files:
frontend/src/config/routes.tsfrontend/src/App.tsxfrontend/src/services/api/contracts.tsfrontend/src/services/api/templates.tsfrontend/src/pages/dashboard/templates/TemplateMakeTaskPage.tsxfrontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsxfrontend/src/__tests__/App.test.tsxfrontend/src/i18n/locales/zh/translation.jsonfrontend/src/i18n/locales/ja/translation.jsonInterfaces:
GET /templates/:templateIdPATCH /templates/:templateIdProduces:
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"
Files:
frontend/src/pages/dashboard/templates/TemplateEditPage.tsxfrontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsxfrontend/src/i18n/locales/zh/translation.jsonfrontend/src/i18n/locales/ja/translation.jsonInterfaces:
getTemplateBasicInfo(templateId: string): Promise<TemplateBasicInfo>updateTemplateBasicInfo(templateId: string, input: UpdateTemplateBasicInfoInput): Promise<TemplateBasicInfo>routes.templateEditProduces:
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"
Files:
frontend/src/pages/dashboard/templates/TemplateDetailPage.tsxfrontend/src/pages/dashboard/templates/__tests__/TemplateDetailPage.test.tsxfrontend/src/pages/dashboard/templates/__tests__/TemplateMakeTaskPage.test.tsxfrontend/src/pages/dashboard/templates/__tests__/TemplateEditPage.test.tsxfrontend/src/__tests__/App.test.tsxbackend/src/modules/templates/templates.service.spec.tsbackend/test/templates.e2e-spec.tsInterfaces:
Produces:
[ ] 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:
npm run typecheck PASSBackend 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"
TBD, TODO, or deferred implementation text remainsTemplateBasicInfo / TemplateBasicInfoDto shapeUpdateTemplateBasicInfoInput / UpdateTemplateBasicInfoDto write contract