From 8a5402aa4acd698333142677aef0125fb8ae6fa3 Mon Sep 17 00:00:00 2001 From: Abbas Naqvi Date: Mon, 29 Sep 2025 19:03:36 +0530 Subject: [PATCH 1/5] fixed default repo functionality --- server/plugin/api.go | 49 +++++++++++--- server/plugin/command.go | 7 +- webapp/src/actions/index.ts | 4 +- webapp/src/client/client.ts | 4 +- .../github_repo_selector.jsx | 64 ++++++++++++++----- .../components/github_repo_selector/index.js | 3 + 6 files changed, 100 insertions(+), 31 deletions(-) diff --git a/server/plugin/api.go b/server/plugin/api.go index b5fc63c1c..272135f9c 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -37,7 +37,8 @@ const ( requestTimeout = 30 * time.Second oauthCompleteTimeout = 2 * time.Minute - channelIDParam = "channelId" + channelIDParam = "channelId" + organisationParam = "organization" ) type OAuthState struct { @@ -1441,7 +1442,7 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re opt := github.ListOptions{PerPage: 50} - orgString := r.URL.Query().Get("organization") + orgString := r.URL.Query().Get(organisationParam) if orgString == "" { c.Log.Warnf("Organization query param is empty") @@ -1449,6 +1450,13 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re return } + channelIDString := r.URL.Query().Get(channelIDParam) + if channelIDString == "" { + c.Log.Warnf("Channel ID query param is empty") + p.writeAPIError(w, &APIErrorResponse{Message: "Channel ID query is empty, must include Channel ID ", StatusCode: http.StatusBadRequest}) + return + } + orgList := strings.Split(orgString, ",") var allRepos []*github.Repository @@ -1489,21 +1497,44 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re } // Only send repositories which are part of the requested organization(s) - type RepositoryResponse struct { - Name string `json:"name,omitempty"` - FullName string `json:"full_name,omitempty"` - Permissions map[string]bool `json:"permissions,omitempty"` - } - resp := make([]*RepositoryResponse, len(allRepos)) + repoResp := make([]RepoResponse, len(allRepos)) for i, r := range allRepos { - resp[i] = &RepositoryResponse{ + repoResp[i] = RepoResponse{ Name: r.GetName(), FullName: r.GetFullName(), Permissions: r.GetPermissions(), } } + resp := RepositoryResponse{ + Repos: repoResp, + } + + // Add default repo if available + defaultRepo, dErr := p.GetDefaultRepo(c.GHInfo.UserID, channelIDString) + if dErr != nil { + c.Log.WithError(dErr).Warnf("Failed to get the default repo for the channel. UserID: %s. ChannelID: %s", c.GHInfo.UserID, channelIDString) + } + + if defaultRepo != "" { + config := p.getConfiguration() + baseURL := config.getBaseURL() + owner, repo := parseOwnerAndRepo(defaultRepo, baseURL) + defaultRepository, err := getRepository(c.Ctx, owner, repo, githubClient) + if err != nil { + c.Log.WithError(err).Warnf("Failed to get the default repo %s/%s", owner, repo) + } + + if defaultRepository != nil { + resp.DefaultRepo = RepoResponse{ + Name: defaultRepository.GetName(), + FullName: defaultRepository.GetFullName(), + Permissions: defaultRepository.Permissions, + } + } + } + p.writeJSON(w, resp) } diff --git a/server/plugin/command.go b/server/plugin/command.go index bdf3fe90d..87f80692d 100644 --- a/server/plugin/command.go +++ b/server/plugin/command.go @@ -6,6 +6,7 @@ package plugin import ( "context" "fmt" + "net/http" "strings" "unicode" @@ -195,11 +196,15 @@ func (p *Plugin) isValidGitHubUsername(username string, userInfo *GitHubUserInfo if cErr := p.useGitHubClient(userInfo, func(userInfo *GitHubUserInfo, token *oauth2.Token) error { ghUser, _, err := githubClient.Users.Get(context.Background(), username) if err != nil { + if gErr, ok := err.(*github.ErrorResponse); ok && gErr.Response.StatusCode == http.StatusNotFound { + return ErrNotFound + } + return err } if ghUser == nil { - return fmt.Errorf("%w", ErrNotFound) + return ErrNotFound } return nil diff --git a/webapp/src/actions/index.ts b/webapp/src/actions/index.ts index aa0a6165e..420b0ab2d 100644 --- a/webapp/src/actions/index.ts +++ b/webapp/src/actions/index.ts @@ -100,11 +100,11 @@ export function getOrgs() { }; } -export function getReposByOrg(organization: string) { +export function getReposByOrg(organization: string, channelId: string) { return async (dispatch: DispatchFunc) => { let data; try { - data = await Client.getRepositoriesByOrganization(organization); + data = await Client.getRepositoriesByOrganization(organization, channelId); } catch (error) { return {error: data}; } diff --git a/webapp/src/client/client.ts b/webapp/src/client/client.ts index 89855ed89..fc656d435 100644 --- a/webapp/src/client/client.ts +++ b/webapp/src/client/client.ts @@ -37,8 +37,8 @@ export default class Client { return this.doGet(`${this.url}/repositories?channelId=${channelId}`); } - getRepositoriesByOrganization = async (organization: string): Promise => { - return this.doGet(`${this.url}/repos_by_org?organization=${organization}`); + getRepositoriesByOrganization = async (organization: string, channelId: string): Promise => { + return this.doGet(`${this.url}/repos_by_org?organization=${organization}&channelId=${channelId}`); } getPrsDetails = async (prList: {url: string, number: number}[]) => { diff --git a/webapp/src/components/github_repo_selector/github_repo_selector.jsx b/webapp/src/components/github_repo_selector/github_repo_selector.jsx index a260a0775..7d24201b1 100644 --- a/webapp/src/components/github_repo_selector/github_repo_selector.jsx +++ b/webapp/src/components/github_repo_selector/github_repo_selector.jsx @@ -6,16 +6,10 @@ import PropTypes from 'prop-types'; import ReactSelectSetting from '@/components/react_select_setting'; -const initialState = { - invalid: false, - error: null, - org: '', -}; - export default class GithubRepoSelector extends PureComponent { static propTypes = { yourOrgs: PropTypes.array.isRequired, - yourReposByOrg: PropTypes.array, + yourReposByOrg: PropTypes.object, theme: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, value: PropTypes.string, @@ -28,39 +22,75 @@ export default class GithubRepoSelector extends PureComponent { }).isRequired, }; + static defaultProps = { + yourReposByOrg: {repos: []}, + }; + constructor(props) { super(props); - this.state = initialState; + this.state = {org: ''}; } componentDidMount() { this.props.actions.getOrgs(); } + getReposArray = () => { + const {yourReposByOrg} = this.props; + + if (yourReposByOrg?.repos?.length > 0) { + return yourReposByOrg.repos; + } + + if (yourReposByOrg?.defaultRepo) { + return [yourReposByOrg.defaultRepo]; + } + + return []; + } + componentDidUpdate(prevProps) { - if (prevProps.yourOrgs !== this.props.yourOrgs) { - if (this.props.yourOrgs.length) { - this.onChangeForOrg(0, this.props.yourOrgs[0].login); + const repos = this.getReposArray(); + const defaultRepo = this.props.yourReposByOrg?.defaultRepo; + const prevDefaultRepo = prevProps.yourReposByOrg?.defaultRepo; + + if ((!this.props.value || (defaultRepo && defaultRepo.full_name !== prevDefaultRepo?.full_name)) && defaultRepo) { + this.onChangeForRepo(defaultRepo.name, defaultRepo.full_name); + } else if (!defaultRepo && !this.props.value && repos.length > 0) { + this.onChangeForRepo(repos[0].name, repos[0].full_name); + } + + if (prevProps.yourOrgs !== this.props.yourOrgs && this.props.yourOrgs.length > 0) { + const newOrg = this.props.yourOrgs[0].login; + if (this.state.org !== newOrg) { + this.setState({org: newOrg}); + this.onChangeForOrg(newOrg); } } } - onChangeForOrg = (_, org) => { + onChangeForOrg = (org) => { if (this.state.org !== org) { this.setState({org}); - this.props.actions.getReposByOrg(org); + this.props.actions.getReposByOrg(org, this.props.currentChannelId); this.props.onChange(null); } } onChangeForRepo = (_, name) => { - const repo = this.props.yourReposByOrg.find((r) => r.full_name === name); - this.props.onChange({name, permissions: repo.permissions}); + const repos = this.getReposArray(); + + const repo = repos.find((r) => r.full_name === name); + if (repo) { + this.props.onChange({name, permissions: repo.permissions}); + } } render() { - const orgOptions = this.props.yourOrgs.map((item) => ({value: item.login, label: item.login})); - const repoOptions = this.props.yourReposByOrg.map((item) => ({value: item.full_name, label: item.name})); + const orgOptions = this.props.yourOrgs.map((org) => ({value: org.login, label: org.login})); + + const repos = this.getReposArray(); + const repoOptions = repos.map((repo) => ({value: repo.full_name, label: repo.name})); let orgSelector = null; let helperTextForRepoSelector = 'Returns GitHub repositories connected to the user account'; diff --git a/webapp/src/components/github_repo_selector/index.js b/webapp/src/components/github_repo_selector/index.js index 4837f0439..bee378eb0 100644 --- a/webapp/src/components/github_repo_selector/index.js +++ b/webapp/src/components/github_repo_selector/index.js @@ -4,6 +4,8 @@ import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; + import manifest from '@/manifest'; import {getReposByOrg, getOrgs} from '../../actions'; @@ -14,6 +16,7 @@ function mapStateToProps(state) { return { yourOrgs: state[`plugins-${manifest.id}`].yourOrgs, yourReposByOrg: state[`plugins-${manifest.id}`].yourReposByOrg, + currentChannelId: getCurrentChannelId(state), }; } From 659846dd237ca66b63a725ae0506c1437f523483 Mon Sep 17 00:00:00 2001 From: Abbas Naqvi Date: Mon, 29 Sep 2025 19:17:50 +0530 Subject: [PATCH 2/5] fix lint --- .../src/components/github_repo_selector/github_repo_selector.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/webapp/src/components/github_repo_selector/github_repo_selector.jsx b/webapp/src/components/github_repo_selector/github_repo_selector.jsx index 7d24201b1..551def73f 100644 --- a/webapp/src/components/github_repo_selector/github_repo_selector.jsx +++ b/webapp/src/components/github_repo_selector/github_repo_selector.jsx @@ -63,7 +63,6 @@ export default class GithubRepoSelector extends PureComponent { if (prevProps.yourOrgs !== this.props.yourOrgs && this.props.yourOrgs.length > 0) { const newOrg = this.props.yourOrgs[0].login; if (this.state.org !== newOrg) { - this.setState({org: newOrg}); this.onChangeForOrg(newOrg); } } From fdb8ae3bc978c0c765927bb45f9dfe2213962fec Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Fri, 3 Oct 2025 16:24:19 -0400 Subject: [PATCH 3/5] Update server/plugin/api.go --- server/plugin/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/plugin/api.go b/server/plugin/api.go index 272135f9c..495a7ef5f 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -1446,7 +1446,7 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re if orgString == "" { c.Log.Warnf("Organization query param is empty") - p.writeAPIError(w, &APIErrorResponse{Message: "Organization query is empty, must include organization name ", StatusCode: http.StatusBadRequest}) + p.writeAPIError(w, &APIErrorResponse{Message: "Organization query parameter is empty, must include organization name ", StatusCode: http.StatusBadRequest}) return } From 6b8a4740827d953ce20070e38a75dcf7defec9f1 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Fri, 3 Oct 2025 16:24:27 -0400 Subject: [PATCH 4/5] Update server/plugin/api.go --- server/plugin/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/plugin/api.go b/server/plugin/api.go index 495a7ef5f..513d7c4ae 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -1453,7 +1453,7 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re channelIDString := r.URL.Query().Get(channelIDParam) if channelIDString == "" { c.Log.Warnf("Channel ID query param is empty") - p.writeAPIError(w, &APIErrorResponse{Message: "Channel ID query is empty, must include Channel ID ", StatusCode: http.StatusBadRequest}) + p.writeAPIError(w, &APIErrorResponse{Message: "Channel ID query parameter is empty, must include Channel ID ", StatusCode: http.StatusBadRequest}) return } From 8f7e2c0a2cf6d70b045d2249ebe3817a669b4f0a Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Fri, 3 Oct 2025 16:25:55 -0400 Subject: [PATCH 5/5] Update server/plugin/api.go --- server/plugin/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/plugin/api.go b/server/plugin/api.go index 513d7c4ae..20746e87b 100644 --- a/server/plugin/api.go +++ b/server/plugin/api.go @@ -1453,7 +1453,7 @@ func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Re channelIDString := r.URL.Query().Get(channelIDParam) if channelIDString == "" { c.Log.Warnf("Channel ID query param is empty") - p.writeAPIError(w, &APIErrorResponse{Message: "Channel ID query parameter is empty, must include Channel ID ", StatusCode: http.StatusBadRequest}) + p.writeAPIError(w, &APIErrorResponse{Message: "ChannelId query parameter is empty, must include Channel ID ", StatusCode: http.StatusBadRequest}) return }