feat: teams within a project

Projects keep teams of members in their settings (settings.teams; people
outside the project are dropped, teams are archived, not removed; absent
when saving means unchanged). Tickets go to a team (`team`, also filtered
in the list and in MCP search_issues), and a comment mentioning
@[Frontend](team-FRONT) tells each of its members. Settings has the teams
with their members, the ticket a «Equipo» select, the list a team filter,
and «@» offers the teams after the people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 16:20:35 +02:00
parent 4acf629106
commit 869acd3f1f
41 changed files with 895 additions and 13 deletions

View File

@@ -854,6 +854,16 @@ const MAX_FILE_BYTES = 25 * 1024 * 1024;
>
<pi-label>Área</pi-label>
</pi-select-field>
@if (teamOptions().length > 1) {
<pi-select-field
[options]="teamOptions()"
[disabled]="!project.canContribute()"
[value]="issue.team ?? ''"
(valueChange)="update({ team: $any($event || null) })"
>
<pi-label>Equipo</pi-label>
</pi-select-field>
}
@for (field of customFields(); track field.id) {
<ts-custom-field
[field]="field"
@@ -1076,6 +1086,18 @@ export class IssuePage {
.map((t) => ({ value: t.id, label: t.name }));
},
);
protected readonly teamOptions = computed<PiSelectFieldOption<string>[]>(
() => {
const current = this.facade.issue()?.team;
return [
{ value: NONE, label: 'Sin equipo' },
...this.project
.workflow()
.settings.teams.filter((t) => !t.archived || t.id === current)
.map((t) => ({ value: t.id, label: t.name })),
];
},
);
protected readonly areaOptions = computed<PiSelectFieldOption<string>[]>(
() => {
const workflow = this.project.workflow();
@@ -1164,9 +1186,7 @@ export class IssuePage {
protected readonly pasteImage: ImageUploader = (file) =>
this.facade.uploadImage(file);
/** Who `@` offers in what is written here. */
protected readonly mentionable = computed(() =>
this.project.members().map((m) => m.user),
);
protected readonly mentionable = this.project.mentionable;
/** Pull requests first, then the newest. */
protected readonly devLinks = computed(() =>
[...(this.facade.issue()?.devLinks ?? [])].sort(
@@ -1332,6 +1352,7 @@ export class IssuePage {
type?: string;
priority?: IssuePriority;
area?: string | null;
team?: string | null;
storyPoints?: number | null;
startDate?: string | null;
dueDate?: string | null;