diff --git a/github/enum/Modals.ts b/github/enum/Modals.ts index 489320a..833afe2 100644 --- a/github/enum/Modals.ts +++ b/github/enum/Modals.ts @@ -1,23 +1,38 @@ export enum ModalsEnum { ADD_GITHUB_ISSUE_ASSIGNEE_PROFILE = 'add-github-issue-assignee-profile', SHARE_ISSUE_ACTION = 'share-issue-action', + SHARE_PULL_REQUEST_ACTION = 'share-pull-request-action', TRIGGER_ISSUE_DISPLAY_MODAL = 'display-issue', + TRIGGER_PULL_REQUEST_DISPLAY_MODAL = 'display-pull-request', SWITCH_ISSUE_ORDER = 'switch-issue-order', ISSUES_ASCENDING = 'asc', + PULL_REQUESTS_ASCENDING = 'asc', ISSUES_DESCENDING = 'desc', + PULL_REQUESTS_DESCENDING = 'desc', SWITCH_ISSUE_STATE = 'switch-issue-state', + SWITCH_PULL_REQUEST_STATE = 'switch-pull-request-state', SWITCH_ISSUE_SORT = 'switch-issue-sort', + SWITCH_PULL_REQUEST_SORT = 'switch-pull-request-sort', ISSUE_SORT_CREATED = 'sort_created', + PULL_REQUEST_SORT_CREATED = 'sort_created', ISSUE_SORT_UPDATED = 'sort_updated', + PULL_REQUEST_SORT_UPDATED = 'sort_updated', ISSUE_SORT_COMMENTS = 'sort_comments', + PULL_REQUEST_SORT_COMMENTS = 'sort_comments', ISSUE_STATE_OPEN = 'open', + PULL_REQUEST_STATE_OPEN = 'open', ISSUE_STATE_CLOSED = 'closed', + PULL_REQUEST_STATE_CLOSED = 'closed', ASSIGNED_ISSUE_FILTER = 'assigned', CREATED_ISSUE_FILTER = 'created', + CREATED_PULL_REQUEST_FILTER = 'created', MENTIONED_ISSUE_FILTER = 'mentioned', + MENTIONED_PULL_REQUEST_FILTER = 'mentioned', SWITCH_ISSUE_FILTER = 'switch-issue-filter', + SWITCH_PULL_REQUEST_FILTER = 'switch-pull-request-filter', USER_ISSUE_VIEW = 'user-issue-view', TRIGGER_ISSUES_MODAL = 'trigger-issue-modal', + TRIGGER_PULL_REQUESTS_MODAL = 'trigger-pull-request-modal', TRIGGER_REPOS_MODAL = 'trigger-repos-modal', TRIGGER_ACTIVITY_MODAL = 'trigger-activity-modal', TRIGGER_NOTIFICATIONS_MODAL = 'trigger-notifications-modal', diff --git a/github/enum/OcticonIcons.ts b/github/enum/OcticonIcons.ts index 0174b80..921f013 100644 --- a/github/enum/OcticonIcons.ts +++ b/github/enum/OcticonIcons.ts @@ -1,6 +1,8 @@ export enum OcticonIcons { ISSUE_OPEN = "https://raw.githubusercontent.com/primer/octicons/main/icons/issue-opened-24.svg", + PULL_REQUEST_OPEN = "https://raw.githubusercontent.com/primer/octicons/main/icons/git-pull-request-24.svg", ISSUE_CLOSED = "https://raw.githubusercontent.com/primer/octicons/main/icons/issue-closed-24.svg", + PULL_REQUEST_CLOSED = "https://raw.githubusercontent.com/primer/octicons/main/icons/git-pull-request-24.svg", COMMENTS = "https://raw.githubusercontent.com/primer/octicons/main/icons/comment-24.svg", COMMIT = "https://raw.githubusercontent.com/primer/octicons/main/icons/commit-24.svg", PERSON = "https://raw.githubusercontent.com/primer/octicons/main/icons/person-24.svg", diff --git a/github/handlers/ExecuteBlockActionHandler.ts b/github/handlers/ExecuteBlockActionHandler.ts index 93c3c88..21c548e 100644 --- a/github/handlers/ExecuteBlockActionHandler.ts +++ b/github/handlers/ExecuteBlockActionHandler.ts @@ -44,7 +44,8 @@ import { IGitHubIssueData } from "../definitions/githubIssueData"; import { shareProfileModal } from "../modals/profileShareModal"; import { RocketChatAssociationModel, RocketChatAssociationRecord } from "@rocket.chat/apps-engine/definition/metadata"; import { userIssuesModal } from "../modals/UserIssuesModal"; -import { IssueDisplayModal } from "../modals/IssueDisplayModal"; +import { userPullRequestsModal } from "../modals/UserPullRequestsModal"; +import { IssueAndPullRequestDisplayModal } from "../modals/IssueAndPullRequestDisplayModal"; import { IGitHubIssue } from "../definitions/githubIssue"; import { BodyMarkdownRenderer } from "../processors/bodyMarkdowmRenderer"; import { CreateIssueStatsBar } from "../lib/CreateIssueStatsBar"; @@ -119,6 +120,76 @@ export class ExecuteBlockActionHandler { }; } } + case ModalsEnum.SHARE_PULL_REQUEST_ACTION: { + let {user, value, room} = context.getInteractionData(); + const access_token = await getAccessTokenForUser(this.read, user, this.app.oauth2Config) as IAuthData; + + const repoName = value?.split(",")[0] ?? ""; + const issueNumber = value?.split(",")[1] ?? ""; + + const issueInfo : IGitHubIssue = await getIssueData(repoName, issueNumber, access_token.token, this.http); + + const block = this.modify.getCreator().getBlockBuilder(); + + CreateIssueStatsBar(issueInfo, block); + + block.addSectionBlock({ + text : { + text : `*${issueInfo.title}*` ?? "", + type : TextObjectType.MARKDOWN + } + }), + block.addActionsBlock({ + elements : [ + block.newButtonElement({ + text : { + text : "Open Pull Request in Browser", + type : TextObjectType.PLAINTEXT + }, + url : issueInfo.html_url, + style : ButtonStyle.PRIMARY + }) + ] + }) + issueInfo.body && BodyMarkdownRenderer({body : issueInfo.body, block : block}); + + if(user?.id){ + if(room?.id){ + await sendMessage(this.modify, room!, user, `Pull Request`, block) + }else{ + let roomId = ( + await getInteractionRoomData( + this.read.getPersistenceReader(), + user.id + ) + ).roomId; + room = await this.read.getRoomReader().getById(roomId) as IRoom; + await sendMessage(this.modify, room, user, `Pull Request`, block) + } + } + break; + } + case ModalsEnum.TRIGGER_PULL_REQUEST_DISPLAY_MODAL : { + const {user, value} = context.getInteractionData(); + const access_token = await getAccessTokenForUser(this.read, user, this.app.oauth2Config) as IAuthData; + + const repoInfo = value?.split(",")[0] ?? ""; + const issueNumber = value?.split(",")[1] ?? ""; + + const issueDisplayModal = await IssueAndPullRequestDisplayModal({ + repoName : repoInfo, + issueNumber : issueNumber, + access_token : access_token.token, + modify : this.modify, + read : this.read, + persistence : this.persistence, + http : this.http, + uikitcontext : context, + isIssue:false + }) + + return context.getInteractionResponder().updateModalViewResponse(issueDisplayModal); + } case ModalsEnum.SHARE_ISSUE_ACTION : { let {user, value, room} = context.getInteractionData(); const access_token = await getAccessTokenForUser(this.read, user, this.app.oauth2Config) as IAuthData; @@ -175,7 +246,7 @@ export class ExecuteBlockActionHandler { const repoInfo = value?.split(",")[0] ?? ""; const issueNumber = value?.split(",")[1] ?? ""; - const issueDisplayModal = await IssueDisplayModal({ + const issueDisplayModal = await IssueAndPullRequestDisplayModal({ repoName : repoInfo, issueNumber : issueNumber, access_token : access_token.token, @@ -183,7 +254,8 @@ export class ExecuteBlockActionHandler { read : this.read, persistence : this.persistence, http : this.http, - uikitcontext : context + uikitcontext : context, + isIssue:true }) return context.getInteractionResponder().updateModalViewResponse(issueDisplayModal); @@ -256,6 +328,96 @@ export class ExecuteBlockActionHandler { return context.getInteractionResponder().updateModalViewResponse(issueModal); } + case ModalsEnum.SWITCH_PULL_REQUEST_SORT : + case ModalsEnum.SWITCH_PULL_REQUEST_STATE : + case ModalsEnum.SWITCH_PULL_REQUEST_FILTER : { + const record = new RocketChatAssociationRecord(RocketChatAssociationModel.MISC, "PULL_REQUEST_MAIN_FILTER"); + + const pullRequestFilterArray = await this.read.getPersistenceReader().readByAssociation(record) as {filter: string, state: string, sort: string, order: string}[]; + + const {user, value} = context.getInteractionData(); + + let filter: {filter: string, state: string, sort: string, order: string} | undefined; + + const prev_sort = pullRequestFilterArray.length == 0 ? ModalsEnum.PULL_REQUEST_SORT_CREATED : pullRequestFilterArray[0].sort; + const prev_filter = pullRequestFilterArray.length == 0 ? ModalsEnum.CREATED_PULL_REQUEST_FILTER : pullRequestFilterArray[0].filter; + const prev_state = pullRequestFilterArray.length == 0 ? ModalsEnum.PULL_REQUEST_STATE_OPEN : pullRequestFilterArray[0].state; + + switch (value as string) { + case ModalsEnum.MENTIONED_PULL_REQUEST_FILTER: + case ModalsEnum.CREATED_PULL_REQUEST_FILTER: + filter = { + filter: value as string, + sort: prev_sort, + state: prev_state, + order: ModalsEnum.PULL_REQUESTS_DESCENDING + }; + break; + case ModalsEnum.PULL_REQUEST_SORT_CREATED: + case ModalsEnum.PULL_REQUEST_SORT_COMMENTS: + case ModalsEnum.PULL_REQUEST_SORT_UPDATED: + filter = { + filter: prev_filter, + sort: value as string, + state: prev_state, + order: ModalsEnum.PULL_REQUESTS_DESCENDING + }; + break; + case ModalsEnum.PULL_REQUEST_STATE_OPEN: + case ModalsEnum.PULL_REQUEST_STATE_CLOSED: + filter = { + filter: prev_filter, + sort: prev_sort, + state: value as string, + order: ModalsEnum.PULL_REQUESTS_DESCENDING + }; + break; + default: + filter = { + filter: ModalsEnum.CREATED_PULL_REQUEST_FILTER, + sort: ModalsEnum.PULL_REQUEST_SORT_CREATED, + state: ModalsEnum.PULL_REQUEST_STATE_OPEN, + order: ModalsEnum.PULL_REQUESTS_DESCENDING + }; + } + + let access_token = await getAccessTokenForUser(this.read, user, this.app.oauth2Config) as IAuthData; + const pullRequestModal = await userPullRequestsModal({ + access_token: access_token.token, + filter: filter, + modify: this.modify, + read: this.read, + persistence: this.persistence, + http: this.http + }); + + await this.persistence.updateByAssociation(record, filter); + + return context.getInteractionResponder().updateModalViewResponse(pullRequestModal); + } + + case ModalsEnum.TRIGGER_PULL_REQUESTS_MODAL : { + const {user} = context.getInteractionData(); + + let access_token = await getAccessTokenForUser(this.read, user, this.app.oauth2Config) as IAuthData; + + const filter = { + filter : ModalsEnum.CREATED_PULL_REQUEST_FILTER, + state : ModalsEnum.PULL_REQUEST_STATE_OPEN, + sort : ModalsEnum.PULL_REQUEST_SORT_CREATED + } + + const pullRequestModal = await userPullRequestsModal({ + filter : filter, + access_token : access_token.token, + modify: this.modify, + read : this.read, + persistence : this.persistence, + http : this.http, + uikitcontext : context + }); + return context.getInteractionResponder().openModalViewResponse(pullRequestModal); + } case ModalsEnum.TRIGGER_ISSUES_MODAL : { const {user} = context.getInteractionData(); diff --git a/github/helpers/githubSDK.ts b/github/helpers/githubSDK.ts index 2574a72..dea1747 100644 --- a/github/helpers/githubSDK.ts +++ b/github/helpers/githubSDK.ts @@ -555,6 +555,58 @@ export async function getUserAssignedIssues( } } +export async function getUserPullRequests( + http: IHttp, + username: String, + access_token: String, + filter: { + filter: String, + state: String, + sort: String + }, +): Promise { + + let url; + + switch (filter.filter) { + case ModalsEnum.CREATED_PULL_REQUEST_FILTER: + url = `https://api.github.com/search/issues?q=is:${filter.state}+is:pr+sort:${filter.sort.substring(5)}-desc+author:${username}` + break; + case ModalsEnum.MENTIONED_PULL_REQUEST_FILTER: + url = `https://api.github.com/search/issues?q=is:${filter.state}+is:pr+sort:${filter.sort.substring(5)}-desc+mentions:${username}` + default: + break; + } + try { + const response = await getRequest( + http, + access_token, + url, + ); + + const modifiedResponse: Array = response.items.map((value): IGitHubIssue => { + return { + issue_id: value.id as string, + issue_compact: value.body as string, + repo_url: value.repository_url as string, + user_login: value.user.login as string, + user_avatar: value.user.avatar_url as string, + number: value.number as number, + title: value.title as string, + body: value.body as string, + state: value.state as string, + last_updated_at: value.updated_at as string, + comments: value.comments as number, + } + }) + return modifiedResponse; + } + catch (e) { + return []; + } +} + + export async function getIssueData( repoInfo: String, issueNumber: String, diff --git a/github/modals/IssueDisplayModal.ts b/github/modals/IssueAndPullRequestDisplayModal.ts similarity index 73% rename from github/modals/IssueDisplayModal.ts rename to github/modals/IssueAndPullRequestDisplayModal.ts index 89d480e..9cd3265 100644 --- a/github/modals/IssueDisplayModal.ts +++ b/github/modals/IssueAndPullRequestDisplayModal.ts @@ -12,7 +12,7 @@ import { CreateReactionsBar } from "../lib/CreateReactionsBar"; import { getInteractionRoomData, storeInteractionRoomData } from "../persistance/roomInteraction"; import { BodyMarkdownRenderer } from "../processors/bodyMarkdowmRenderer"; -export async function IssueDisplayModal ({ +export async function IssueAndPullRequestDisplayModal ({ repoName, issueNumber, access_token, @@ -21,7 +21,8 @@ export async function IssueDisplayModal ({ persistence, http, slashcommandcontext, - uikitcontext + uikitcontext, + isIssue=true } : { repoName : String, issueNumber : String, @@ -31,7 +32,8 @@ export async function IssueDisplayModal ({ persistence: IPersistence, http: IHttp, slashcommandcontext?: SlashCommandContext, - uikitcontext?: UIKitInteractionContext + uikitcontext?: UIKitInteractionContext, + isIssue : boolean }) : Promise { const viewId = ModalsEnum.USER_ISSUE_VIEW; const block = modify.getCreator().getBlockBuilder(); @@ -97,26 +99,37 @@ export async function IssueDisplayModal ({ issueInfo.body && BodyMarkdownRenderer({body : issueInfo.body, block : block}) block.addActionsBlock({ - elements : [ - block.newButtonElement({ - actionId : ModalsEnum.SHARE_ISSUE_ACTION, - value : `${repoName}, ${issueNumber}`, - text : { - text : "Share Issue", - type : TextObjectType.PLAINTEXT - }, - }), - block.newButtonElement({ - actionId : ModalsEnum.ADD_GITHUB_ISSUE_ASSIGNEE, - value : `${repoName}, ${issueNumber}`, - text : { - text : "Assign Issue", - type : TextObjectType.PLAINTEXT - }, - }) + elements: [ + ...(isIssue ? [ + block.newButtonElement({ + actionId: ModalsEnum.SHARE_ISSUE_ACTION, + value: `${repoName}, ${issueNumber}`, + text: { + text: `Share Issue`, + type: TextObjectType.PLAINTEXT + }, + }), + block.newButtonElement({ + actionId: ModalsEnum.ADD_GITHUB_ISSUE_ASSIGNEE, + value: `${repoName}, ${issueNumber}`, + text: { + text: "Assign Issue", + type: TextObjectType.PLAINTEXT + }, + }) + ] : [ + block.newButtonElement({ + actionId: ModalsEnum.SHARE_PULL_REQUEST_ACTION, + value: `${repoName}, ${issueNumber}`, + text: { + text: `Share Pull Request`, + type: TextObjectType.PLAINTEXT + }, + }) + ]) ] - }) - + }); + return { id : viewId, title : { diff --git a/github/modals/UserProfileModal.ts b/github/modals/UserProfileModal.ts index 77ca4cf..576df61 100644 --- a/github/modals/UserProfileModal.ts +++ b/github/modals/UserProfileModal.ts @@ -105,6 +105,17 @@ export async function userProfileModal({ }, style: ButtonStyle.PRIMARY }, + ), + block.newButtonElement( + { + actionId: ModalsEnum.TRIGGER_PULL_REQUESTS_MODAL, + value: "Trigger Pull Requests Modal", + text: { + type: TextObjectType.PLAINTEXT, + text: "Pull Requests" + }, + style: ButtonStyle.PRIMARY + }, ) ] }) diff --git a/github/modals/UserPullRequestsModal.ts b/github/modals/UserPullRequestsModal.ts new file mode 100644 index 0000000..05fe97e --- /dev/null +++ b/github/modals/UserPullRequestsModal.ts @@ -0,0 +1,266 @@ +import { + IModify, + IRead, + IPersistence, + IHttp, +} from "@rocket.chat/apps-engine/definition/accessors"; +import { RocketChatAssociationModel, RocketChatAssociationRecord } from "@rocket.chat/apps-engine/definition/metadata"; +import { SlashCommandContext } from "@rocket.chat/apps-engine/definition/slashcommands"; +import { + ButtonStyle, + TextObjectType, + UIKitInteractionContext, +} from "@rocket.chat/apps-engine/definition/uikit"; +import { IUIKitModalViewParam } from "@rocket.chat/apps-engine/definition/uikit/UIKitInteractionResponder"; +import { ModalsEnum } from "../enum/Modals"; +import { OcticonIcons } from "../enum/OcticonIcons"; +import { getBasicUserInfo, getUserPullRequests } from "../helpers/githubSDK"; +import { + getInteractionRoomData, + storeInteractionRoomData, +} from "../persistance/roomInteraction"; + +export async function userPullRequestsModal({ + filter, + access_token, + modify, + read, + persistence, + http, + slashcommandcontext, + uikitcontext, +}: { + filter: { + filter: string; + state: string; + sort: string; + }; + access_token: String; + modify: IModify; + read: IRead; + persistence: IPersistence; + http: IHttp; + slashcommandcontext?: SlashCommandContext; + uikitcontext?: UIKitInteractionContext; +}): Promise { + const viewId = ModalsEnum.USER_ISSUE_VIEW; + const block = modify.getCreator().getBlockBuilder(); + const room = + slashcommandcontext?.getRoom() || + uikitcontext?.getInteractionData().room; + const user = + slashcommandcontext?.getSender() || + uikitcontext?.getInteractionData().user; + + if (user?.id) { + let roomId; + if (room?.id) { + roomId = room.id; + await storeInteractionRoomData(persistence, user.id, roomId); + } else { + roomId = ( + await getInteractionRoomData( + read.getPersistenceReader(), + user.id + ) + ).roomId; + } + } + + const userInfo = await getBasicUserInfo(http, access_token); + + const repoInfo = await getUserPullRequests( + http, + userInfo.username, + access_token, + filter + ); + + block.addActionsBlock({ + elements: [ + block.newStaticSelectElement({ + placeholder: { + text: "Select a PR Filter", + type: TextObjectType.PLAINTEXT, + }, + initialValue: filter.filter, + actionId: ModalsEnum.SWITCH_PULL_REQUEST_FILTER, + options: [ + { + value: ModalsEnum.CREATED_PULL_REQUEST_FILTER, + text: { + text: "Created", + type: TextObjectType.PLAINTEXT, + }, + }, + { + value: ModalsEnum.MENTIONED_PULL_REQUEST_FILTER, + text: { + text: "Mentioned", + type: TextObjectType.PLAINTEXT, + }, + }, + ], + }), + ], + }); + + block.addActionsBlock({ + elements: [ + block.newStaticSelectElement({ + placeholder: { + text: "Select Pull Requests State", + type: TextObjectType.PLAINTEXT, + }, + initialValue: filter.state, + actionId: ModalsEnum.SWITCH_PULL_REQUEST_STATE, + options: [ + { + value: ModalsEnum.PULL_REQUEST_STATE_OPEN, + text: { + text: "Open Pull Requests", + type: TextObjectType.PLAINTEXT, + }, + + }, + { + value: ModalsEnum.PULL_REQUEST_STATE_CLOSED, + text: { + text: "Closed Pull Requests", + type: TextObjectType.PLAINTEXT, + }, + } + ], + }), + block.newStaticSelectElement({ + actionId: ModalsEnum.SWITCH_PULL_REQUEST_SORT, + placeholder: { + text: "Sort Pull Requests By...", + type: TextObjectType.PLAINTEXT, + }, + initialValue: filter.sort, + options: [ + { + value: ModalsEnum.PULL_REQUEST_SORT_CREATED, + text: { + text: "Created", + type: TextObjectType.PLAINTEXT, + }, + }, + { + value: ModalsEnum.PULL_REQUEST_SORT_UPDATED, + text: { + text: "Updated", + type: TextObjectType.PLAINTEXT, + }, + }, + { + value: ModalsEnum.PULL_REQUEST_SORT_COMMENTS, + text: { + text: "Comments", + type: TextObjectType.PLAINTEXT, + }, + }, + ], + }), + ], + }); + + if (repoInfo.length == 0) { + block.addContextBlock({ + elements: [ + block.newPlainTextObject( + "Sorry, there are no pull requests to display" + ), + ], + }); + } else { + repoInfo.map( + (value) => { + const repoURL = value.repo_url ?? ""; + const repoName = repoURL.substring(29, repoURL.length); + block.addContextBlock({ + elements: [ + block.newImageElement({ + imageUrl: OcticonIcons.REPOSITORY, + altText: "REPO_ICON", + }), + block.newPlainTextObject(repoName, false), + block.newImageElement({ + imageUrl: value.user_avatar ?? "", + altText: "User Image", + }), + block.newPlainTextObject(value.user_login ?? ""), + ], + }); + block.addSectionBlock({ + text: { + text: `\`#${value.number}\` ${value.title}` ?? "None", + type: TextObjectType.MARKDOWN, + }, + }); + const lastUpdated = new Date(value.last_updated_at ?? ""); + block.addContextBlock({ + elements: [ + block.newImageElement({ + imageUrl: OcticonIcons.COMMENTS, + altText: "Comments", + }), + block.newPlainTextObject( + `${value.comments}`, + false + ), + block.newImageElement({ + imageUrl: + value.state == "open" + ? OcticonIcons.PULL_REQUEST_OPEN + : OcticonIcons.PULL_REQUEST_CLOSED, + altText: "State", + }), + block.newPlainTextObject(`${value.state}`), + block.newImageElement({ + imageUrl: OcticonIcons.PENCIL, + altText: "Last Update At", + }), + block.newPlainTextObject( + `Last Updated at ${lastUpdated.toUTCString()}` + ), + ], + }); + + block.addActionsBlock({ + elements: [ + block.newButtonElement({ + actionId: ModalsEnum.SHARE_PULL_REQUEST_ACTION, + value: `${repoName}, ${value.number}`, + text: { + text: "Share Pull Request", + type: TextObjectType.PLAINTEXT, + }, + style: ButtonStyle.PRIMARY, + }), + block.newButtonElement({ + actionId: ModalsEnum.TRIGGER_PULL_REQUEST_DISPLAY_MODAL, + value: `${repoName}, ${value.number}`, + text: { + text: "Open Pull Request", + type: TextObjectType.PLAINTEXT, + }, + style: ButtonStyle.PRIMARY, + }), + ], + }); + block.addDividerBlock(); + } + ); + } + + return { + id: viewId, + title: { + text: "Your Pull Requests", + type: TextObjectType.PLAINTEXT, + }, + blocks: block.getBlocks(), + }; +}