Skip to content

Add unassigned event #6972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/common/timelineEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum EventType {
Labeled,
Milestoned,
Assigned,
Unassigned,
HeadRefDeleted,
Merged,
CrossReferenced,
Expand Down Expand Up @@ -102,6 +103,14 @@ export interface AssignEvent {
createdAt: string;
}

export interface UnassignEvent {
id: number;
event: EventType.Unassigned;
unassignees: IAccount[];
actor: IActor;
createdAt: string;
}

export interface HeadRefDeleteEvent {
id: string;
event: EventType.HeadRefDeleted;
Expand Down Expand Up @@ -139,4 +148,4 @@ export interface ReopenedEvent {
createdAt: string;
}

export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent;
export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent;
8 changes: 8 additions & 0 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ export interface AssignedEvent {
createdAt: string;
}

export interface UnassignedEvent {
__typename: string;
id: number;
actor: Actor;
user: Account;
createdAt: string;
}

export interface MergeQueueEntry {
position: number;
state: MergeQueueState;
Expand Down
17 changes: 17 additions & 0 deletions src/github/queriesShared.gql
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ fragment AssignedEvent on AssignedEvent {
createdAt
}

fragment UnassignedEvent on UnassignedEvent {
id
actor {
...Node
...Actor
}
user {
...Node
...Actor
...User
}
createdAt
}

fragment CrossReferencedEvent on CrossReferencedEvent {
id
actor {
Expand Down Expand Up @@ -288,6 +302,7 @@ query TimelineEvents($owner: String!, $name: String!, $number: Int!, $last: Int
...Review
...Commit
...AssignedEvent
...UnassignedEvent
...HeadRefDeleted
...CrossReferencedEvent
...ClosedEvent
Expand All @@ -309,6 +324,7 @@ query IssueTimelineEvents($owner: String!, $name: String!, $number: Int!, $last:
__typename
...Comment
...AssignedEvent
...UnassignedEvent
...CrossReferencedEvent
...ClosedEvent
...ReopenedEvent
Expand Down Expand Up @@ -1545,6 +1561,7 @@ mutation MergePullRequest($input: MergePullRequestInput!, $last: Int = 150) {
...Review
...Commit
...AssignedEvent
...UnassignedEvent
...HeadRefDeleted
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/github/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@ export function convertGraphQLEventType(text: string) {
return Common.EventType.Milestoned;
case 'AssignedEvent':
return Common.EventType.Assigned;
case 'UnassignedEvent':
return Common.EventType.Unassigned;
case 'HeadRefDeletedEvent':
return Common.EventType.HeadRefDeleted;
case 'IssueComment':
Expand Down Expand Up @@ -1070,6 +1072,17 @@ export async function parseGraphQLTimelineEvents(
createdAt: assignEv.createdAt,
});
break;
case Common.EventType.Unassigned:
const unassignEv = event as GraphQL.UnassignedEvent;

normalizedEvents.push({
id: unassignEv.id,
event: type,
unassignees: [parseAccount(unassignEv.user, githubRepository)],
actor: parseAccount(unassignEv.actor),
createdAt: unassignEv.createdAt,
});
break;
case Common.EventType.HeadRefDeleted:
const deletedEv = event as GraphQL.HeadRefDeletedEvent;

Expand Down
59 changes: 45 additions & 14 deletions webviews/components/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
ReopenedEvent,
ReviewEvent,
TimelineEvent,
UnassignEvent,
} from '../../src/common/timelineEvent';
import { groupBy, UnreachableCaseError } from '../../src/common/utils';
import { IAccount, IActor } from '../../src/github/interface';
import { ReviewType } from '../../src/github/views';
import PullRequestContext from '../common/context';
import { CommentView } from './comment';
Expand All @@ -28,16 +30,32 @@ import { nbsp } from './space';
import { Timestamp } from './timestamp';
import { AuthorLink, Avatar } from './user';

function isAssignUnassignEvent(event: TimelineEvent | ConsolidatedAssignUnassignEvent): event is AssignEvent | UnassignEvent {
return event.event === EventType.Assigned || event.event === EventType.Unassigned;
}

interface ConsolidatedAssignUnassignEvent {
id: number;
event: EventType.Assigned | EventType.Unassigned;
assignees?: IAccount[];
unassignees?: IAccount[];
actor: IActor;
createdAt: string;
}

export const Timeline = ({ events, isIssue }: { events: TimelineEvent[], isIssue: boolean }) => {
const consolidatedEvents: TimelineEvent[] = [];
const consolidatedEvents: (TimelineEvent | ConsolidatedAssignUnassignEvent)[] = [];
for (let i = 0; i < events.length; i++) {
if ((i > 0) && (events[i].event === EventType.Assigned) && (consolidatedEvents[consolidatedEvents.length - 1].event === EventType.Assigned)) {
const lastEvent = consolidatedEvents[consolidatedEvents.length - 1] as AssignEvent;
const newEvent = events[i] as AssignEvent;
if (new Date(lastEvent.createdAt).getTime() + (1000 * 60 * 10) > new Date(newEvent.createdAt).getTime()) { // within 10 minutes
if (lastEvent.assignees.every(a => a.id !== newEvent.assignees[0].id)) {
lastEvent.assignees = [...lastEvent.assignees, ...newEvent.assignees];
}
if ((i > 0) && isAssignUnassignEvent(events[i]) && isAssignUnassignEvent(consolidatedEvents[consolidatedEvents.length - 1])) {
const lastEvent = consolidatedEvents[consolidatedEvents.length - 1] as ConsolidatedAssignUnassignEvent;
const newEvent = events[i] as ConsolidatedAssignUnassignEvent;
if ((lastEvent.actor.login === newEvent.actor.login) && (new Date(lastEvent.createdAt).getTime() + (1000 * 60 * 10) > new Date(newEvent.createdAt).getTime())) { // within 10 minutes
const assignees = lastEvent.assignees || [];
const unassignees = lastEvent.unassignees || [];
const newAssignees = newEvent.assignees?.filter(a => !assignees.some(b => b.id === a.id)) ?? [];
const newUnassignees = newEvent.unassignees?.filter(a => !unassignees.some(b => b.id === a.id)) ?? [];
lastEvent.assignees = [...assignees, ...newAssignees];
lastEvent.unassignees = [...unassignees, ...newUnassignees];
lastEvent.createdAt = newEvent.createdAt;
} else {
consolidatedEvents.push(newEvent);
Expand All @@ -58,7 +76,9 @@ export const Timeline = ({ events, isIssue }: { events: TimelineEvent[], isIssue
case EventType.Merged:
return <MergedEventView key={`merged${event.id}`} {...event} />;
case EventType.Assigned:
return <AssignEventView key={`assign${event.id}`} event={event} isIssue={isIssue} />;
return <AssignUnassignEventView key={`assign${event.id}`} event={event} />;
case EventType.Unassigned:
return <AssignUnassignEventView key={`unassign${event.id}`} event={event} />;
case EventType.HeadRefDeleted:
return <HeadDeleteEventView key={`head${event.id}`} {...event} />;
case EventType.CrossReferenced:
Expand Down Expand Up @@ -344,9 +364,22 @@ function joinWithAnd(arr: JSX.Element[]): JSX.Element {
return <>{arr.slice(0, -1).map(item => <>{item}, </>)} and {arr[arr.length - 1]}</>;
}

const AssignEventView = ({ event, isIssue }: { event: AssignEvent, isIssue: boolean }) => {
const { actor, assignees } = event;
const AssignUnassignEventView = ({ event }: { event: AssignEvent | UnassignEvent | ConsolidatedAssignUnassignEvent }) => {
const { actor } = event;
const assignees = (event as AssignEvent).assignees || [];
const unassignees = (event as UnassignEvent).unassignees || [];
const joinedAssignees = joinWithAnd(assignees.map(a => <AuthorLink key={a.id} for={a} />));
const joinedUnassignees = joinWithAnd(unassignees.map(a => <AuthorLink key={a.id} for={a} />));

let message: JSX.Element;
if (assignees.length > 0 && unassignees.length > 0) {
message = <>assigned {joinedAssignees} and unassigned {joinedUnassignees}</>;
} else if (assignees.length > 0) {
message = <>assigned {joinedAssignees}</>;
} else {
message = <>unassigned {joinedUnassignees}</>;
}

return (
<div className="comment-container commit">
<div className="commit-message">
Expand All @@ -355,9 +388,7 @@ const AssignEventView = ({ event, isIssue }: { event: AssignEvent, isIssue: bool
</div>
<AuthorLink for={actor} />
<div className="message">
{isIssue
? <>assigned {joinedAssignees}</>
: <>assigned {joinedAssignees} to this pull request</>}
{message}
</div>
</div>
<Timestamp date={event.createdAt} />
Expand Down