feat: a home page with life
Greeting and summary of the day, the coming week, the latest across my projects, who is online now, the week in numbers (mine and the team's, with a streak), sprints under way, «Retomar», project cards with their activity, what to pick up when nothing is mine and first steps for newcomers. REST (GET /api/me/home), GraphQL (myHome) and MCP (get_my_week). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
ProjectAnalyticsGql,
|
||||
ProjectInsightsGql,
|
||||
PortfolioProjectGql,
|
||||
HomeGql,
|
||||
ProjectGql,
|
||||
ProjectSettingsGql,
|
||||
ProjectStatsGql,
|
||||
@@ -48,6 +49,14 @@ export class ProjectResolver {
|
||||
return this.projects.findMine(me.id);
|
||||
}
|
||||
|
||||
@Query(() => HomeGql, {
|
||||
description:
|
||||
'The home page beyond my work: agenda of the week, activity of my projects, the week in numbers, sprints, project cards, suggestions, first steps and who is on what now',
|
||||
})
|
||||
myHome(@CurrentUser() me: User) {
|
||||
return this.projects.home(me.id);
|
||||
}
|
||||
|
||||
@Query(() => [PortfolioProjectGql], {
|
||||
description:
|
||||
'My active projects (not archived, not as a guest) at a glance: open work, risks, sprint, release forecasts and epics',
|
||||
|
||||
@@ -1097,3 +1097,95 @@ export class PortfolioProjectGql {
|
||||
epics!: EpicGql[];
|
||||
@Field() updatedAt!: string;
|
||||
}
|
||||
|
||||
@ObjectType('AgendaItem', { description: 'Something on a day of my week' })
|
||||
export class AgendaItemGql {
|
||||
@Field({ description: 'YYYY-MM-DD' }) day!: string;
|
||||
@Field({ description: 'ISSUE, SPRINT_END or RELEASE' }) kind!: string;
|
||||
@Field() projectKey!: string;
|
||||
@Field() title!: string;
|
||||
@Field(() => String, { nullable: true }) issueKey!: string | null;
|
||||
}
|
||||
|
||||
@ObjectType('TeamWeek')
|
||||
export class TeamWeekGql {
|
||||
@Field(() => [Int], {
|
||||
description: 'Finished per day, last 14, oldest first',
|
||||
})
|
||||
perDay!: number[];
|
||||
@Field(() => Int) thisWeek!: number;
|
||||
@Field(() => Int) lastWeek!: number;
|
||||
}
|
||||
|
||||
@ObjectType('MyWeek')
|
||||
export class MyWeekGql {
|
||||
@Field(() => Int) thisWeek!: number;
|
||||
@Field(() => Int) lastWeek!: number;
|
||||
@Field(() => Int, { description: 'Days in a row with something finished' })
|
||||
streak!: number;
|
||||
}
|
||||
|
||||
@ObjectType('HomeSprint')
|
||||
export class HomeSprintGql {
|
||||
@Field() projectKey!: string;
|
||||
@Field() projectName!: string;
|
||||
@Field(() => SprintPulseGql) sprint!: SprintPulseGql;
|
||||
}
|
||||
|
||||
@ObjectType('HomeSprintBrief')
|
||||
export class HomeSprintBriefGql {
|
||||
@Field() name!: string;
|
||||
@Field(() => Int) percent!: number;
|
||||
@Field() behind!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType('HomeProject')
|
||||
export class HomeProjectGql {
|
||||
@Field(() => ID) projectId!: string;
|
||||
@Field(() => Int) open!: number;
|
||||
@Field(() => Int) overdue!: number;
|
||||
@Field(() => [Int], {
|
||||
description: 'Activity per day, last 14, oldest first',
|
||||
})
|
||||
activity!: number[];
|
||||
@Field(() => HomeSprintBriefGql, { nullable: true })
|
||||
sprint!: HomeSprintBriefGql | null;
|
||||
}
|
||||
|
||||
@ObjectType('Suggestion')
|
||||
export class SuggestionGql {
|
||||
@Field({ description: 'UNASSIGNED, PROPOSALS or QA_WAITING' }) kind!: string;
|
||||
@Field() projectKey!: string;
|
||||
@Field(() => Int) count!: number;
|
||||
}
|
||||
|
||||
@ObjectType('FirstSteps')
|
||||
export class FirstStepsGql {
|
||||
@Field() project!: boolean;
|
||||
@Field() invited!: boolean;
|
||||
@Field() ticket!: boolean;
|
||||
@Field() calendar!: boolean;
|
||||
}
|
||||
|
||||
@ObjectType('Online', { description: 'Someone of my projects on a ticket now' })
|
||||
export class OnlineGql {
|
||||
@Field(() => UserRefGql) user!: UserRefGql;
|
||||
@Field() projectKey!: string;
|
||||
@Field(() => String, { nullable: true }) issueKey!: string | null;
|
||||
@Field(() => String, { nullable: true }) issueTitle!: string | null;
|
||||
@Field() editing!: boolean;
|
||||
@Field() at!: string;
|
||||
}
|
||||
|
||||
@ObjectType('Home', { description: 'The home page beyond my work' })
|
||||
export class HomeGql {
|
||||
@Field(() => [AgendaItemGql]) agenda!: AgendaItemGql[];
|
||||
@Field(() => [ActivityGql]) activity!: ActivityGql[];
|
||||
@Field(() => TeamWeekGql) team!: TeamWeekGql;
|
||||
@Field(() => MyWeekGql) me!: MyWeekGql;
|
||||
@Field(() => [HomeSprintGql]) sprints!: HomeSprintGql[];
|
||||
@Field(() => [HomeProjectGql]) projects!: HomeProjectGql[];
|
||||
@Field(() => [SuggestionGql]) suggestions!: SuggestionGql[];
|
||||
@Field(() => FirstStepsGql) firstSteps!: FirstStepsGql;
|
||||
@Field(() => [OnlineGql]) online!: OnlineGql[];
|
||||
}
|
||||
|
||||
@@ -1161,3 +1161,88 @@ export class PortfolioProjectDto {
|
||||
epics!: EpicDto[];
|
||||
@ApiProperty() updatedAt!: string;
|
||||
}
|
||||
|
||||
export class AgendaItemDto {
|
||||
@ApiProperty({ format: 'date' }) day!: string;
|
||||
@ApiProperty({ enum: ['ISSUE', 'SPRINT_END', 'RELEASE'] }) kind!: string;
|
||||
@ApiProperty() projectKey!: string;
|
||||
@ApiProperty() title!: string;
|
||||
@ApiProperty({ type: String, nullable: true }) issueKey!: string | null;
|
||||
}
|
||||
|
||||
export class TeamWeekDto {
|
||||
@ApiProperty({
|
||||
type: [Number],
|
||||
description: 'Finished per day, last 14, oldest first',
|
||||
})
|
||||
perDay!: number[];
|
||||
@ApiProperty() thisWeek!: number;
|
||||
@ApiProperty() lastWeek!: number;
|
||||
}
|
||||
|
||||
export class MyWeekDto {
|
||||
@ApiProperty() thisWeek!: number;
|
||||
@ApiProperty() lastWeek!: number;
|
||||
@ApiProperty({ description: 'Days in a row with something finished' })
|
||||
streak!: number;
|
||||
}
|
||||
|
||||
export class HomeSprintDto {
|
||||
@ApiProperty() projectKey!: string;
|
||||
@ApiProperty() projectName!: string;
|
||||
@ApiProperty({ type: SprintPulseDto }) sprint!: SprintPulseDto;
|
||||
}
|
||||
|
||||
export class HomeSprintBriefDto {
|
||||
@ApiProperty() name!: string;
|
||||
@ApiProperty() percent!: number;
|
||||
@ApiProperty() behind!: boolean;
|
||||
}
|
||||
|
||||
export class HomeProjectDto {
|
||||
@ApiProperty({ format: 'uuid' }) projectId!: string;
|
||||
@ApiProperty() open!: number;
|
||||
@ApiProperty() overdue!: number;
|
||||
@ApiProperty({
|
||||
type: [Number],
|
||||
description: 'Activity per day, last 14, oldest first',
|
||||
})
|
||||
activity!: number[];
|
||||
@ApiProperty({ type: HomeSprintBriefDto, nullable: true })
|
||||
sprint!: HomeSprintBriefDto | null;
|
||||
}
|
||||
|
||||
export class SuggestionDto {
|
||||
@ApiProperty({ enum: ['UNASSIGNED', 'PROPOSALS', 'QA_WAITING'] })
|
||||
kind!: string;
|
||||
@ApiProperty() projectKey!: string;
|
||||
@ApiProperty() count!: number;
|
||||
}
|
||||
|
||||
export class FirstStepsDto {
|
||||
@ApiProperty() project!: boolean;
|
||||
@ApiProperty() invited!: boolean;
|
||||
@ApiProperty() ticket!: boolean;
|
||||
@ApiProperty() calendar!: boolean;
|
||||
}
|
||||
|
||||
export class OnlineDto {
|
||||
@ApiProperty({ type: UserRefDto }) user!: UserRefDto;
|
||||
@ApiProperty() projectKey!: string;
|
||||
@ApiProperty({ type: String, nullable: true }) issueKey!: string | null;
|
||||
@ApiProperty({ type: String, nullable: true }) issueTitle!: string | null;
|
||||
@ApiProperty() editing!: boolean;
|
||||
@ApiProperty() at!: string;
|
||||
}
|
||||
|
||||
export class HomeDto {
|
||||
@ApiProperty({ type: [AgendaItemDto] }) agenda!: AgendaItemDto[];
|
||||
@ApiProperty({ type: [ActivityDto] }) activity!: ActivityDto[];
|
||||
@ApiProperty({ type: TeamWeekDto }) team!: TeamWeekDto;
|
||||
@ApiProperty({ type: MyWeekDto }) me!: MyWeekDto;
|
||||
@ApiProperty({ type: [HomeSprintDto] }) sprints!: HomeSprintDto[];
|
||||
@ApiProperty({ type: [HomeProjectDto] }) projects!: HomeProjectDto[];
|
||||
@ApiProperty({ type: [SuggestionDto] }) suggestions!: SuggestionDto[];
|
||||
@ApiProperty({ type: FirstStepsDto }) firstSteps!: FirstStepsDto;
|
||||
@ApiProperty({ type: [OnlineDto] }) online!: OnlineDto[];
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ import {
|
||||
IssueDto,
|
||||
IssuePageDto,
|
||||
PortfolioProjectDto,
|
||||
HomeDto,
|
||||
PresenceDto,
|
||||
} from './dto/responses.dto';
|
||||
import { viewerOf } from '../common/viewer';
|
||||
@@ -95,6 +96,18 @@ export class IssuesController {
|
||||
private readonly projects: ProjectsService,
|
||||
) {}
|
||||
|
||||
@Get('me/home')
|
||||
@ApiOperation({
|
||||
summary: 'The home page beyond my work',
|
||||
description:
|
||||
'Agenda of my week, latest activity of my projects, the week in numbers (the team and me), sprints under way, project cards, suggestions, first steps and who is on what now.',
|
||||
})
|
||||
@ApiOkResponse({ type: HomeDto })
|
||||
@ApiReadErrors()
|
||||
home(@CurrentUser() me: User) {
|
||||
return this.projects.home(me.id);
|
||||
}
|
||||
|
||||
@Get('me/portfolio')
|
||||
@ApiOperation({
|
||||
summary: 'My projects at a glance',
|
||||
|
||||
Reference in New Issue
Block a user