feat: start a project from another one's configuration

Creating a project can name one the creator owns or administers: its
statuses, transitions, types, areas, custom fields and automation rules
are copied once (paused rules stay paused; rules assigning to someone who
is not in the new project are left out). GraphQL templateProjectId, REST
template (id or key), MCP create_project template, and a select in the
web's new-project form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:25:00 +02:00
parent 21bc598590
commit e5f4e6c683
18 changed files with 342 additions and 72 deletions

View File

@@ -55,6 +55,15 @@ export class CreateProjectInput {
@IsOptional()
@IsBoolean()
isPublic?: boolean;
@Field(() => ID, {
nullable: true,
description:
'A project you own or administer to start from: its statuses, transitions, types, areas, custom fields and automation rules are copied',
})
@IsOptional()
@IsUUID()
templateProjectId?: string | null;
}
@InputType()

View File

@@ -151,7 +151,11 @@ export class ProjectResolver {
@CurrentUser() me: User,
@Args('input') input: CreateProjectInput,
) {
return this.projects.create(me.id, input);
const { templateProjectId, ...fields } = input;
return this.projects.create(me.id, {
...fields,
templateId: templateProjectId,
});
}
@Mutation(() => CommandResultGql)

View File

@@ -87,10 +87,21 @@ export class CreateProjectDto {
@IsOptional()
@IsBoolean()
isPublic?: boolean;
@ApiPropertyOptional({
type: String,
nullable: true,
example: 'API',
description:
'A project you own or administer (id or key) to start from: its statuses, transitions, types, areas, custom fields and automation rules are copied',
})
@IsOptional()
@IsString()
template?: string | null;
}
export class UpdateProjectDto extends PartialType(
OmitType(CreateProjectDto, ['key'] as const),
OmitType(CreateProjectDto, ['key', 'template'] as const),
) {
@ApiPropertyOptional({
description:

View File

@@ -83,8 +83,12 @@ export class ProjectsController {
@ApiOperation({ summary: 'Create a project (you become its owner)' })
@ApiCreatedResponse({ type: CommandResultDto })
@ApiCommandErrors()
create(@CurrentUser() me: User, @Body() body: CreateProjectDto) {
return this.projects.create(me.id, body);
async create(@CurrentUser() me: User, @Body() body: CreateProjectDto) {
const { template, ...fields } = body;
return this.projects.create(me.id, {
...fields,
templateId: template ? await this.refs.projectId(me.id, template) : null,
});
}
@Get(':project')