diff --git a/server/plugin/api.go b/server/plugin/api.go index babcfdfb8..fd4381c13 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -97,9 +97,7 @@ func (p *Plugin) initializeAPI() { apiRouter.HandleFunc("/create_issue", p.checkAuth(p.attachUserContext(p.createIssue), ResponseTypePlain)).Methods(http.MethodPost) apiRouter.HandleFunc("/close_or_reopen_issue", p.checkAuth(p.attachUserContext(p.closeOrReopenIssue), ResponseTypePlain)).Methods(http.MethodPost) apiRouter.HandleFunc("/update_issue", p.checkAuth(p.attachUserContext(p.updateIssue), ResponseTypePlain)).Methods(http.MethodPost) - apiRouter.HandleFunc("/edit_issue_modal", p.checkAuth(p.attachUserContext(p.openIssueEditModal), ResponseTypePlain)).Methods(http.MethodPost) - apiRouter.HandleFunc("/close_reopen_issue_modal", p.checkAuth(p.attachUserContext(p.openCloseOrReopenIssueModal), ResponseTypePlain)).Methods(http.MethodPost) - apiRouter.HandleFunc("/attach_comment_issue_modal", p.checkAuth(p.attachUserContext(p.openAttachCommentIssueModal), ResponseTypePlain)).Methods(http.MethodPost) + apiRouter.HandleFunc("/issue_info", p.checkAuth(p.attachUserContext(p.getIssueInfo), ResponseTypePlain)).Methods(http.MethodGet) apiRouter.HandleFunc("/create_issue_comment", p.checkAuth(p.attachUserContext(p.createIssueComment), ResponseTypePlain)).Methods(http.MethodPost) apiRouter.HandleFunc("/mentions", p.checkAuth(p.attachUserContext(p.getMentions), ResponseTypePlain)).Methods(http.MethodGet) apiRouter.HandleFunc("/unreads", p.checkAuth(p.attachUserContext(p.getUnreads), ResponseTypePlain)).Methods(http.MethodGet) @@ -959,99 +957,38 @@ func (p *Plugin) updateSettings(c *serializer.UserContext, w http.ResponseWriter p.writeJSON(w, info.Settings) } -func (p *Plugin) openAttachCommentIssueModal(c *serializer.UserContext, w http.ResponseWriter, r *http.Request) { - req := &serializer.OpenCreateCommentOrEditIssueModalRequestBody{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - c.Log.WithError(err).Warnf("Error decoding the JSON body") - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: "Please provide a valid JSON object.", StatusCode: http.StatusBadRequest}) - return - } - - userID := r.Header.Get(constants.HeaderMattermostUserID) - post, appErr := p.API.GetPost(req.PostID) - if appErr != nil { - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s", req.PostID), StatusCode: http.StatusInternalServerError}) - return - } - if post == nil { - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s : not found", req.PostID), StatusCode: http.StatusNotFound}) - return - } - - p.API.PublishWebSocketEvent( - wsEventAttachCommentToIssue, - map[string]interface{}{ - "postId": post.Id, - "owner": req.RepoOwner, - "repo": req.RepoName, - "number": req.IssueNumber, - }, - &model.WebsocketBroadcast{UserId: userID}, - ) -} - -func (p *Plugin) openCloseOrReopenIssueModal(c *serializer.UserContext, w http.ResponseWriter, r *http.Request) { - req := &serializer.OpenCreateCommentOrEditIssueModalRequestBody{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - c.Log.WithError(err).Warnf("Error decoding the JSON body") - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: "Please provide a valid JSON object.", StatusCode: http.StatusBadRequest}) - return - } - - userID := r.Header.Get(constants.HeaderMattermostUserID) - - post, appErr := p.API.GetPost(req.PostID) - if appErr != nil { - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s", req.PostID), StatusCode: http.StatusInternalServerError}) - return - } - if post == nil { - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s : not found", req.PostID), StatusCode: http.StatusNotFound}) - return - } - - p.API.PublishWebSocketEvent( - wsEventCloseOrReopenIssue, - map[string]interface{}{ - "channel_id": post.ChannelId, - "owner": req.RepoOwner, - "repo": req.RepoName, - "number": req.IssueNumber, - "status": req.Status, - "postId": req.PostID, - }, - &model.WebsocketBroadcast{UserId: userID}, - ) -} +func (p *Plugin) getIssueInfo(c *serializer.UserContext, w http.ResponseWriter, r *http.Request) { + owner := r.FormValue(constants.OwnerQueryParam) + repo := r.FormValue(constants.RepoQueryParam) + number := r.FormValue(constants.NumberQueryParam) + postID := r.FormValue(constants.PostIDQueryParam) -func (p *Plugin) openIssueEditModal(c *serializer.UserContext, w http.ResponseWriter, r *http.Request) { - req := &serializer.OpenCreateCommentOrEditIssueModalRequestBody{} - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - c.Log.WithError(err).Warnf("Error decoding the JSON body") - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: "Please provide a valid JSON object.", StatusCode: http.StatusBadRequest}) + issueNumber, err := strconv.Atoi(number) + if err != nil { + p.writeAPIError(w, &serializer.APIErrorResponse{Message: "Invalid param 'number'.", StatusCode: http.StatusBadRequest}) return } githubClient := p.githubConnectUser(c.Context.Ctx, c.GHInfo) - issue, _, err := githubClient.Issues.Get(c.Ctx, req.RepoOwner, req.RepoName, req.IssueNumber) + issue, _, err := githubClient.Issues.Get(c.Ctx, owner, repo, issueNumber) if err != nil { // If the issue is not found, it probably belongs to a private repo. // Return an empty response in that case. var gerr *github.ErrorResponse if errors.As(err, &gerr) && gerr.Response.StatusCode == http.StatusNotFound { c.Log.WithError(err).With(logger.LogContext{ - "owner": req.RepoOwner, - "repo": req.RepoName, - "number": req.IssueNumber, + "owner": owner, + "repo": repo, + "number": issueNumber, }).Debugf("Issue not found") p.writeJSON(w, nil) return } c.Log.WithError(err).With(logger.LogContext{ - "owner": req.RepoOwner, - "repo": req.RepoName, - "number": req.IssueNumber, + "owner": owner, + "repo": repo, + "number": issueNumber, }).Debugf("Could not get the issue") p.writeAPIError(w, &serializer.APIErrorResponse{Message: "Could not get the issue", StatusCode: http.StatusInternalServerError}) return @@ -1080,35 +1017,30 @@ func (p *Plugin) openIssueEditModal(c *serializer.UserContext, w http.ResponseWr milestoneNumber = *issue.Milestone.Number } - userID := r.Header.Get(constants.HeaderMattermostUserID) - post, appErr := p.API.GetPost(req.PostID) + post, appErr := p.API.GetPost(postID) if appErr != nil { - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s", req.PostID), StatusCode: http.StatusInternalServerError}) + p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s", postID), StatusCode: http.StatusInternalServerError}) return } if post == nil { - p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s : not found", req.PostID), StatusCode: http.StatusNotFound}) + p.writeAPIError(w, &serializer.APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load the post %s : not found", postID), StatusCode: http.StatusNotFound}) return } - p.API.PublishWebSocketEvent( - wsEventCreateOrUpdateIssue, - map[string]interface{}{ - "title": *issue.Title, - "channel_id": post.ChannelId, - "postId": req.PostID, - "milestone_title": milestoneTitle, - "milestone_number": milestoneNumber, - "assignees": assignees, - "labels": labels, - "description": description, - "repo_full_name": fmt.Sprintf("%s/%s", req.RepoOwner, req.RepoName), - "issue_number": *issue.Number, - }, - &model.WebsocketBroadcast{UserId: userID}, - ) + issueInfo := map[string]interface{}{ + "title": *issue.Title, + "channel_id": post.ChannelId, + "postId": postID, + "milestone_title": milestoneTitle, + "milestone_number": milestoneNumber, + "assignees": assignees, + "labels": labels, + "description": description, + "repo_full_name": fmt.Sprintf("%s/%s", owner, repo), + "issue_number": *issue.Number, + } - p.writeJSON(w, issue) + p.writeJSON(w, issueInfo) } func (p *Plugin) getIssueByNumber(c *serializer.UserContext, w http.ResponseWriter, r *http.Request) { diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index 0414be207..13d14a725 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -40,11 +40,9 @@ const ( wsEventConnect = "connect" wsEventDisconnect = "disconnect" // WSEventConfigUpdate is the WebSocket event to update the configurations on webapp. - WSEventConfigUpdate = "config_update" - wsEventRefresh = "refresh" - wsEventCreateOrUpdateIssue = "createOrUpdateIssue" - wsEventCloseOrReopenIssue = "closeOrReopenIssue" - wsEventAttachCommentToIssue = "attachCommentToIssue" + WSEventConfigUpdate = "config_update" + wsEventRefresh = "refresh" + wsEventCreateIssue = "createIssue" WSEventRefresh = "refresh" @@ -482,7 +480,7 @@ func (p *Plugin) disconnectGitHubAccount(userID string) { func (p *Plugin) openIssueCreateModal(userID string, channelID string, title string) { p.API.PublishWebSocketEvent( - wsEventCreateOrUpdateIssue, + wsEventCreateIssue, map[string]interface{}{ "title": title, "channel_id": channelID, diff --git a/server/serializer/issue.go b/server/serializer/issue.go index e21432423..a5d400a79 100644 --- a/server/serializer/issue.go +++ b/server/serializer/issue.go @@ -42,11 +42,3 @@ type CommentAndCloseRequest struct { Status string `json:"status"` PostID string `json:"postId"` } - -type OpenCreateCommentOrEditIssueModalRequestBody struct { - RepoOwner string `json:"repo_owner"` - RepoName string `json:"repo_name"` - IssueNumber int `json:"issue_number"` - PostID string `json:"postId"` - Status string `json:"status"` -} diff --git a/webapp/src/actions/index.js b/webapp/src/actions/index.js index 17935a1e9..d6133bb2a 100644 --- a/webapp/src/actions/index.js +++ b/webapp/src/actions/index.js @@ -211,47 +211,11 @@ export function getMilestoneOptions(repo) { }; } -export function attachCommentIssueModal(payload) { +export function getIssueInfo(owner, repo, issueNumber, postID) { return async (dispatch, getState) => { let data; try { - data = await Client.attachCommentIssueModal(payload); - } catch (error) { - return {error}; - } - - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); - if (!connected) { - return {error: data}; - } - - return {data}; - }; -} - -export function editIssueModal(payload) { - return async (dispatch, getState) => { - let data; - try { - data = await Client.editIssueModal(payload); - } catch (error) { - return {error}; - } - - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); - if (!connected) { - return {error: data}; - } - - return {data}; - }; -} - -export function closeOrReopenIssueModal(payload) { - return async (dispatch, getState) => { - let data; - try { - data = await Client.closeOrReopenIssueModal(payload); + data = await Client.getIssueInfo(owner, repo, issueNumber, postID); } catch (error) { return {error}; } diff --git a/webapp/src/client/client.js b/webapp/src/client/client.js index 9b26d88e6..c20b18439 100644 --- a/webapp/src/client/client.js +++ b/webapp/src/client/client.js @@ -7,16 +7,8 @@ import {ClientError} from 'mattermost-redux/client/client4'; import {id as pluginId} from '../manifest'; export default class Client { - editIssueModal = async (payload) => { - return this.doPost(`${this.url}/edit_issue_modal`, payload); - } - - closeOrReopenIssueModal = async (payload) => { - return this.doPost(`${this.url}/close_reopen_issue_modal`, payload); - } - - attachCommentIssueModal = async (payload) => { - return this.doPost(`${this.url}/attach_comment_issue_modal`, payload); + getIssueInfo = async (owner, repo, issueNumber, postID) => { + return this.doGet(`${this.url}/issue_info?owner=${owner}&repo=${repo}&number=${issueNumber}&postId=${postID}`); } setServerRoute(url) { diff --git a/webapp/src/components/github_issue/index.tsx b/webapp/src/components/github_issue/index.tsx index 9edeee267..75eb03873 100644 --- a/webapp/src/components/github_issue/index.tsx +++ b/webapp/src/components/github_issue/index.tsx @@ -4,7 +4,7 @@ import {Theme} from 'mattermost-redux/types/preferences'; import {Post} from 'mattermost-redux/types/posts'; import {useDispatch} from 'react-redux'; -import {attachCommentIssueModal, editIssueModal, closeOrReopenIssueModal} from '../../actions'; +import {openCreateCommentOnIssueModal, openCreateOrUpdateIssueModal, openCloseOrReopenIssueModal} from '../../actions'; type GithubIssueProps = { theme: Theme, @@ -24,6 +24,7 @@ const GithubIssue = ({theme, post}: GithubIssueProps) => { issue_number: postProps.issue_number, postId: post.id, status: postProps.status, + channel_id: post.channel_id, }; const content = ( @@ -31,17 +32,17 @@ const GithubIssue = ({theme, post}: GithubIssueProps) => { ); diff --git a/webapp/src/components/modals/attach_comment_to_issue/attach_comment_to_issue.jsx b/webapp/src/components/modals/attach_comment_to_issue/attach_comment_to_issue.jsx index f959f884a..a674b6cac 100644 --- a/webapp/src/components/modals/attach_comment_to_issue/attach_comment_to_issue.jsx +++ b/webapp/src/components/modals/attach_comment_to_issue/attach_comment_to_issue.jsx @@ -40,11 +40,11 @@ export default class AttachIssueModal extends PureComponent { } if (!this.state.issueValue) { - const {owner, repo, number} = this.props.messageData ?? {}; + const {repo_owner, repo_name, issue_number} = this.props.messageData ?? {}; const issue = { - owner, - repo, - number, + owner: repo_owner, + repo: repo_name, + number: issue_number, comment: this.state.comment, post_id: this.props.post.id, show_attached_message: false, @@ -72,7 +72,7 @@ export default class AttachIssueModal extends PureComponent { owner, repo, number, - comment: this.props.post.message, + comment: this.state.comment, post_id: this.props.post.id, show_attached_message: true, }; @@ -106,17 +106,23 @@ export default class AttachIssueModal extends PureComponent { }); }; + componentDidUpdate(prevProps) { + if (this.props.post && !this.props.messageData && !prevProps.post) { + this.setState({comment: this.props.post.message}); // eslint-disable-line react/no-did-update-set-state + } + } + render() { const {error, submitting, comment, issueValue} = this.state; - const {visible, theme, messageData, post} = this.props; + const {visible, theme, messageData} = this.props; const style = getStyle(theme); if (!visible) { return null; } - const {number} = messageData ?? {}; - const modalTitle = number ? 'Create a comment to GitHub Issue' : 'Attach Message to GitHub Issue'; - const component = number ? ( + const {issue_number} = messageData ?? {}; + const modalTitle = issue_number ? 'Create a comment to GitHub Issue' : 'Attach Message to GitHub Issue'; + const component = issue_number ? (
); diff --git a/webapp/src/components/modals/close_reopen_issue/index.tsx b/webapp/src/components/modals/close_reopen_issue/index.tsx index 5e793a19a..a78c4cb25 100644 --- a/webapp/src/components/modals/close_reopen_issue/index.tsx +++ b/webapp/src/components/modals/close_reopen_issue/index.tsx @@ -36,9 +36,9 @@ const CloseOrReopenIssueModal = ({theme}: {theme: Theme}) => { channel_id: messageData.channel_id, issue_comment: comment, status_reason: currentStatus, - repo: messageData.repo, - number: messageData.number, - owner: messageData.owner, + repo: messageData.repo_name, + number: messageData.issue_number, + owner: messageData.repo_owner, status: messageData.status, postId: messageData.postId, }; diff --git a/webapp/src/components/modals/create_update_issue/create_update_issue.jsx b/webapp/src/components/modals/create_update_issue/create_update_issue.jsx index 2a8bbab83..7ad0983f1 100644 --- a/webapp/src/components/modals/create_update_issue/create_update_issue.jsx +++ b/webapp/src/components/modals/create_update_issue/create_update_issue.jsx @@ -22,6 +22,7 @@ const initialState = { repo: null, issueTitle: '', issueDescription: '', + channelId: '', labels: [], assignees: [], milestone: null, @@ -34,6 +35,7 @@ export default class CreateOrUpdateIssueModal extends PureComponent { update: PropTypes.func.isRequired, close: PropTypes.func.isRequired, create: PropTypes.func.isRequired, + getIssueInfo: PropTypes.func.isRequired, post: PropTypes.object, theme: PropTypes.object.isRequired, visible: PropTypes.bool.isRequired, @@ -46,36 +48,50 @@ export default class CreateOrUpdateIssueModal extends PureComponent { this.validator = new Validator(); } + getIssueInfo = async () => { + const {repo_owner, repo_name, issue_number, postId} = this.props.messageData; + const issueInfo = await this.props.getIssueInfo(repo_owner, repo_name, issue_number, postId); + return issueInfo; + } + + updateState(issueInfo) { + const {channel_id, title, description, milestone_title, milestone_number, repo_full_name} = issueInfo ?? {}; + const assignees = issueInfo?.assignees ?? []; + const labels = issueInfo?.labels ?? []; + + this.setState({milestone: { + value: milestone_number, + label: milestone_title, + }, + repo: { + name: repo_full_name, + }, + assignees, + labels, + channelId: channel_id, + issueDescription: description, + issueTitle: title.substring(0, MAX_TITLE_LENGTH)}); + } + /* eslint-disable react/no-did-update-set-state*/ componentDidUpdate(prevProps) { - if (this.props.post && !this.props.messageData) { + if (this.props.post && !this.props.messageData && !prevProps.post) { this.setState({issueDescription: this.props.post.message}); } - const {channel_id, title, description, assignees, labels, milestone_title, milestone_number, repo_full_name} = this.props.messageData ?? {}; - if (channel_id && (channel_id !== prevProps.messageData?.channel_id || title !== prevProps.messageData?.title || description !== prevProps.messageData?.description || assignees !== prevProps.messageData?.assignees || labels !== prevProps.messageData?.labels || milestone_title !== prevProps.messageData?.milestone_title || milestone_number !== prevProps.messageData?.milestone_number)) { - if (assignees) { - this.setState({assignees}); - } - if (labels) { - this.setState({labels}); - } - this.setState({milestone: { - value: milestone_number, - label: milestone_title, - }, - repo: { - name: repo_full_name, - }, - issueDescription: description, - issueTitle: title.substring(0, MAX_TITLE_LENGTH)}); + if (this.props.messageData?.repo_owner && !prevProps.visible && this.props.visible) { + this.getIssueInfo().then((issueInfo) => { + this.updateState(issueInfo.data); + }); + } else if (this.props.messageData?.channel_id && (this.props.messageData?.channel_id !== prevProps.messageData?.channel_id || this.props.messageData?.title !== prevProps.messageData?.title)) { + this.updateState(this.props.messageData); } } /* eslint-enable */ // handle issue creation or updation after form is populated handleCreateOrUpdate = async (e) => { - const {channel_id, issue_number, repo_full_name} = this.props.messageData ?? {}; + const {issue_number} = this.props.messageData ?? {}; if (e && e.preventDefault) { e.preventDefault(); } @@ -99,7 +115,7 @@ export default class CreateOrUpdateIssueModal extends PureComponent { assignees: this.state.assignees, milestone: this.state.milestone && this.state.milestone.value, post_id: postId, - channel_id, + channel_id: this.state.channelId, issue_number, }; @@ -107,7 +123,7 @@ export default class CreateOrUpdateIssueModal extends PureComponent { issue.repo = this.state.repo; } this.setState({submitting: true}); - if (repo_full_name) { + if (issue_number) { const updated = await this.props.update(issue); if (updated?.error) { const errMessage = getErrorMessage(updated.error.message); @@ -233,6 +249,7 @@ export default class CreateOrUpdateIssueModal extends PureComponent { onChange={this.handleIssueTitleChange} /> {issueTitleValidationError} + {this.renderIssueAttributeSelectors()} bindActionCreators({ close: closeCreateOrUpdateIssueModal, create: createIssue, update: updateIssue, + getIssueInfo, }, dispatch); export default connect(mapStateToProps, mapDispatchToProps)(CreateOrUpdateIssueModal); diff --git a/webapp/src/index.js b/webapp/src/index.js index 0a7a18a42..9b79a57da 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -15,7 +15,7 @@ import GithubIssue from './components/github_issue'; import Reducer from './reducers'; import Client from './client'; import {getConnected, setShowRHSAction} from './actions'; -import {handleConnect, handleDisconnect, handleConfigurationUpdate, handleOpenCreateOrUpdateIssueModal, handleOpenCreateCommentOnIssueModal, handleOpenCloseOrReopenIssueModal, handleReconnect, handleRefresh} from './websocket'; +import {handleConnect, handleDisconnect, handleConfigurationUpdate, handleOpenCreateOrUpdateIssueModal, handleReconnect, handleRefresh} from './websocket'; import {getServerRoute} from './selectors'; import {id as pluginId} from './manifest'; @@ -47,9 +47,7 @@ class PluginClass { registry.registerWebSocketEventHandler(`custom_${pluginId}_disconnect`, handleDisconnect(store)); registry.registerWebSocketEventHandler(`custom_${pluginId}_config_update`, handleConfigurationUpdate(store)); registry.registerWebSocketEventHandler(`custom_${pluginId}_refresh`, handleRefresh(store)); - registry.registerWebSocketEventHandler(`custom_${pluginId}_createOrUpdateIssue`, handleOpenCreateOrUpdateIssueModal(store)); - registry.registerWebSocketEventHandler(`custom_${pluginId}_attachCommentToIssue`, handleOpenCreateCommentOnIssueModal(store)); - registry.registerWebSocketEventHandler(`custom_${pluginId}_closeOrReopenIssue`, handleOpenCloseOrReopenIssueModal(store)); + registry.registerWebSocketEventHandler(`custom_${pluginId}_createIssue`, handleOpenCreateOrUpdateIssueModal(store)); registry.registerPostTypeComponent('custom_git_issue', GithubIssue); registry.registerReconnectHandler(handleReconnect(store)); diff --git a/webapp/src/websocket/index.js b/webapp/src/websocket/index.js index 88690dc55..3a91692cf 100644 --- a/webapp/src/websocket/index.js +++ b/webapp/src/websocket/index.js @@ -10,8 +10,6 @@ import { getYourAssignments, getYourPrs, openCreateOrUpdateIssueModal, - openCloseOrReopenIssueModal, - openCreateCommentOnIssueModal, } from '../actions'; import {id as pluginId} from '../manifest'; @@ -95,21 +93,3 @@ export function handleOpenCreateOrUpdateIssueModal(store) { store.dispatch(openCreateOrUpdateIssueModal(msg.data)); }; } - -export function handleOpenCloseOrReopenIssueModal(store) { - return (msg) => { - if (!msg.data) { - return; - } - store.dispatch(openCloseOrReopenIssueModal(msg.data)); - }; -} - -export function handleOpenCreateCommentOnIssueModal(store) { - return (msg) => { - if (!msg.data) { - return; - } - store.dispatch(openCreateCommentOnIssueModal(msg.data)); - }; -}