diff --git a/webapp/src/action_types/index.js b/webapp/src/action_types/index.ts similarity index 100% rename from webapp/src/action_types/index.js rename to webapp/src/action_types/index.ts diff --git a/webapp/src/actions/index.js b/webapp/src/actions/index.ts similarity index 66% rename from webapp/src/actions/index.js rename to webapp/src/actions/index.ts index d1b11931..186b79fd 100644 --- a/webapp/src/actions/index.js +++ b/webapp/src/actions/index.ts @@ -2,13 +2,19 @@ import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/ import {PostTypes} from 'mattermost-redux/action_types'; +import {AnyAction, Dispatch} from 'redux'; + import Client from '../client'; import ActionTypes from '../action_types'; -import manifest from '../manifest'; +import {APIError, ConnectedData, GitlabUsersData, LHSData, ShowRhsPluginActionData, SubscriptionData} from 'src/types'; +import {Item} from 'src/types/gitlab_items'; +import {GlobalState} from 'src/types/store'; +import {getPluginState} from 'src/selectors'; +import {CommentBody, IssueBody} from 'src/types/gitlab_types'; export function getConnected(reminder = false) { - return async (dispatch) => { - let data; + return async (dispatch: Dispatch) => { + let data: ConnectedData; try { data = await Client.getConnected(reminder); } catch (error) { @@ -24,8 +30,8 @@ export function getConnected(reminder = false) { }; } -function checkAndHandleNotConnected(data) { - return async (dispatch) => { +function checkAndHandleNotConnected(data: APIError) { + return async (dispatch: Dispatch) => { if (data && data.id === 'not_connected') { dispatch({ type: ActionTypes.RECEIVED_CONNECTED, @@ -34,7 +40,7 @@ function checkAndHandleNotConnected(data) { gitlab_username: '', gitlab_client_id: '', settings: {}, - }, + } as ConnectedData, }); return false; } @@ -42,16 +48,16 @@ function checkAndHandleNotConnected(data) { }; } -export function getReviewDetails(prList) { - return async (dispatch, getState) => { - let data; +export function getReviewDetails(prList: Item[]) { + return async (dispatch: Dispatch) => { + let data: Item | APIError; try { data = await Client.getPrsDetails(prList); } catch (error) { return {error}; } - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -65,16 +71,16 @@ export function getReviewDetails(prList) { }; } -export function getYourPrDetails(prList) { - return async (dispatch, getState) => { - let data; +export function getYourPrDetails(prList: Item[]) { + return async (dispatch: Dispatch) => { + let data: Item | APIError; try { data = await Client.getPrsDetails(prList); } catch (error) { return {error}; } - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -88,45 +94,16 @@ export function getYourPrDetails(prList) { }; } -export function getMentions() { - return async (dispatch, getState) => { - let data; - try { - data = await Client.getMentions(); - } catch (error) { - return {error}; - } - - const connected = await checkAndHandleNotConnected(data)( - dispatch, - getState, - ); - if (!connected) { - return {error: data}; - } - - dispatch({ - type: ActionTypes.RECEIVED_MENTIONS, - data, - }); - - return {data}; - }; -} - export function getLHSData() { - return async (dispatch, getState) => { - let data; + return async (dispatch: Dispatch) => { + let data: LHSData | APIError; try { data = await Client.getLHSData(); } catch (error) { return {error}; } - const connected = await checkAndHandleNotConnected(data)( - dispatch, - getState, - ); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -144,14 +121,14 @@ export function getLHSData() { * Stores "showRHSPlugin" action returned by * "registerRightHandSidebarComponent" in plugin initialization. */ -export function setShowRHSAction(showRHSPluginAction) { +export function setShowRHSAction(showRHSPluginAction: () => ShowRhsPluginActionData) { return { type: ActionTypes.RECEIVED_SHOW_RHS_ACTION, showRHSPluginAction, }; } -export function updateRHSState(rhsState) { +export function updateRHSState(rhsState: string) { return { type: ActionTypes.UPDATE_RHS_STATE, state: rhsState, @@ -160,13 +137,13 @@ export function updateRHSState(rhsState) { const GITLAB_USER_GET_TIMEOUT_MILLISECONDS = 1000 * 60 * 60; // 1 hour -export function getGitlabUser(userID) { - return async (dispatch, getState) => { +export function getGitlabUser(userID: string) { + return async (dispatch: Dispatch, getState: () => GlobalState) => { if (!userID) { return {}; } - const user = getState()[`plugins-${manifest.id}`].gitlabUsers[userID]; + const user = getPluginState(getState()).gitlabUsers[userID]; if ( user && user.last_try && @@ -179,11 +156,11 @@ export function getGitlabUser(userID) { return {data: user}; } - let data; + let data: GitlabUsersData; try { data = await Client.getGitlabUser(userID); - } catch (error) { - if (error.status === 404) { + } catch (error: unknown) { + if ((error as APIError).status === 404) { dispatch({ type: ActionTypes.RECEIVED_GITLAB_USER, userID, @@ -203,7 +180,7 @@ export function getGitlabUser(userID) { }; } -export function openCreateIssueModal(postId) { +export function openCreateIssueModal(postId: string) { return { type: ActionTypes.OPEN_CREATE_ISSUE_MODAL, data: { @@ -212,7 +189,7 @@ export function openCreateIssueModal(postId) { }; } -export function openCreateIssueModalWithoutPost(title, channelId) { +export function openCreateIssueModalWithoutPost(title: string, channelId: string) { return { type: ActionTypes.OPEN_CREATE_ISSUE_MODAL_WITHOUT_POST, data: { @@ -228,8 +205,8 @@ export function closeCreateIssueModal() { }; } -export function createIssue(payload) { - return async (dispatch) => { +export function createIssue(payload: IssueBody) { + return async (dispatch: Dispatch) => { let data; try { data = await Client.createIssue(payload); @@ -237,7 +214,7 @@ export function createIssue(payload) { return {error}; } - const connected = await dispatch(checkAndHandleNotConnected(data)); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -245,7 +222,7 @@ export function createIssue(payload) { }; } -export function openAttachCommentToIssueModal(postId) { +export function openAttachCommentToIssueModal(postId: string) { return { type: ActionTypes.OPEN_ATTACH_COMMENT_TO_ISSUE_MODAL, data: { @@ -260,8 +237,8 @@ export function closeAttachCommentToIssueModal() { }; } -export function attachCommentToIssue(payload) { - return async (dispatch) => { +export function attachCommentToIssue(payload: CommentBody) { + return async (dispatch: Dispatch) => { let data; try { data = await Client.attachCommentToIssue(payload); @@ -269,7 +246,7 @@ export function attachCommentToIssue(payload) { return {error}; } - const connected = await dispatch(checkAndHandleNotConnected(data)); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -278,7 +255,7 @@ export function attachCommentToIssue(payload) { } export function getProjects() { - return async (dispatch, getState) => { + return async (dispatch: Dispatch) => { let data; try { data = await Client.getProjects(); @@ -286,7 +263,7 @@ export function getProjects() { return {error}; } - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -300,8 +277,8 @@ export function getProjects() { }; } -export function getLabelOptions(projectID) { - return async (dispatch, getState) => { +export function getLabelOptions(projectID: number) { + return async (dispatch: Dispatch) => { let data; try { data = await Client.getLabels(projectID); @@ -309,7 +286,7 @@ export function getLabelOptions(projectID) { return {error}; } - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -318,8 +295,8 @@ export function getLabelOptions(projectID) { }; } -export function getMilestoneOptions(projectID) { - return async (dispatch, getState) => { +export function getMilestoneOptions(projectID: number) { + return async (dispatch: Dispatch) => { let data; try { data = await Client.getMilestones(projectID); @@ -327,7 +304,7 @@ export function getMilestoneOptions(projectID) { return {error}; } - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -336,8 +313,8 @@ export function getMilestoneOptions(projectID) { }; } -export function getAssigneeOptions(projectID) { - return async (dispatch, getState) => { +export function getAssigneeOptions(projectID: number) { + return async (dispatch: Dispatch, getState: () => GlobalState) => { let data; try { data = await Client.getAssignees(projectID); @@ -345,7 +322,7 @@ export function getAssigneeOptions(projectID) { return {error}; } - const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + const connected = await checkAndHandleNotConnected(data as APIError)(dispatch); if (!connected) { return {error: data}; } @@ -354,13 +331,13 @@ export function getAssigneeOptions(projectID) { }; } -export function getChannelSubscriptions(channelId) { - return async (dispatch) => { +export function getChannelSubscriptions(channelId: string) { + return async (dispatch: Dispatch) => { if (!channelId) { return {}; } - let subscriptions; + let subscriptions: SubscriptionData; try { subscriptions = await Client.getChannelSubscriptions(channelId); } catch (error) { @@ -379,8 +356,8 @@ export function getChannelSubscriptions(channelId) { }; } -export function sendEphemeralPost(message) { - return (dispatch, getState) => { +export function sendEphemeralPost(message: string) { + return (dispatch: Dispatch, getState: () => GlobalState) => { const timestamp = Date.now(); const state = getState(); diff --git a/webapp/src/client/client.js b/webapp/src/client/client.ts similarity index 50% rename from webapp/src/client/client.js rename to webapp/src/client/client.ts index 8bda5c54..ab7781cf 100644 --- a/webapp/src/client/client.js +++ b/webapp/src/client/client.ts @@ -1,40 +1,56 @@ import {Client4} from 'mattermost-redux/client'; import {ClientError} from 'mattermost-redux/client/client4'; +import {Options} from 'mattermost-redux/types/client4'; + +import {Item, TooltipData} from 'src/types/gitlab_items'; +import {APIError, ConnectedData, GitlabUsersData, LHSData, SubscriptionData} from 'src/types'; +import {CommentBody, IssueBody} from 'src/types/gitlab_types'; + export default class Client { - setServerRoute = (url) => { + private url = ''; + + setServerRoute(url: string): void { this.url = `${url}/api/v1`; } getConnected = async (reminder = false) => { - return this.doGet(`${this.url}/connected?reminder=` + reminder); + return this.doGet(`${this.url}/connected?reminder=` + reminder); }; - getPrsDetails = async (prList) => { - return this.doPost(`${this.url}/prdetails`, prList); - } + getPrsDetails = async (prList: Item[]) => { + return this.doPost(`${this.url}/prdetails`, prList); + }; - getLHSData= async () => { - return this.doGet(`${this.url}/lhs-data`); - } + getLHSData = async () => { + return this.doGet(`${this.url}/lhs-data`); + }; - getMentions = async () => { - return this.doGet(`${this.url}/mentions`); + getGitlabUser = async (userID: string) => { + return this.doPost(`${this.url}/user`, {user_id: userID}); }; - getGitlabUser = async (userID) => { - return this.doPost(`${this.url}/user`, {user_id: userID}); + getIssue = async (owner: string, repo: string, issueNumber: string) => { + return this.doGet(`${this.url}/issue?owner=${owner}&repo=${repo}&number=${issueNumber}`); }; - createIssue = async (payload) => { + getPullRequest = async (owner: string, repo: string, prNumber: string) => { + return this.doGet(`${this.url}/mergerequest?owner=${owner}&repo=${repo}&number=${prNumber}`); + }; + + getChannelSubscriptions = async (channelID: string) => { + return this.doGet(`${this.url}/channel/${channelID}/subscriptions`); + }; + + createIssue = async (payload: IssueBody) => { return this.doPost(`${this.url}/issue`, payload); } - attachCommentToIssue = async (payload) => { + attachCommentToIssue = async (payload: CommentBody) => { return this.doPost(`${this.url}/attachcommenttoissue`, payload); } - searchIssues = async (searchTerm) => { + searchIssues = async (searchTerm: string) => { return this.doGet(`${this.url}/searchissues?search=${searchTerm}`); } @@ -42,34 +58,22 @@ export default class Client { return this.doGet(`${this.url}/projects`); } - getLabels = async (projectID) => { + getLabels = async (projectID: number) => { return this.doGet(`${this.url}/labels?projectID=${projectID}`); } - getMilestones = async (projectID) => { + getMilestones = async (projectID: number) => { return this.doGet(`${this.url}/milestones?projectID=${projectID}`); } - getAssignees = async (projectID) => { + getAssignees = async (projectID: number) => { return this.doGet(`${this.url}/assignees?projectID=${projectID}`); } - getIssue = async (owner, repo, issueNumber) => { - return this.doGet(`${this.url}/issue?owner=${owner}&repo=${repo}&number=${issueNumber}`); - } - - getPullRequest = async (owner, repo, prNumber) => { - return this.doGet(`${this.url}/mergerequest?owner=${owner}&repo=${repo}&number=${prNumber}`); - } - - getChannelSubscriptions = async (channelID) => { - return this.doGet(`${this.url}/channel/${channelID}/subscriptions`); - }; - - doGet = async (url, body, headers = {}) => { - headers['X-Timezone-Offset'] = new Date().getTimezoneOffset(); + private async doGet(url: string, headers: { [x: string]: string; } = {}): Promise { + headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset()); - const options = { + const options: Options = { method: 'get', headers, }; @@ -87,12 +91,12 @@ export default class Client { status_code: response.status, url, }); - }; + } - doPost = async (url, body, headers = {}) => { - headers['X-Timezone-Offset'] = new Date().getTimezoneOffset(); + private async doPost(url: string, body: Object, headers: { [x: string]: string; } = {}): Promise { + headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset()); - const options = { + const options: Options = { method: 'post', body: JSON.stringify(body), headers, @@ -111,12 +115,12 @@ export default class Client { status_code: response.status, url, }); - }; + } - doDelete = async (url, body, headers = {}) => { - headers['X-Timezone-Offset'] = new Date().getTimezoneOffset(); + private async doDelete(url: string, headers: { [x: string]: string; } = {}): Promise { + headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset()); - const options = { + const options: Options = { method: 'delete', headers, }; @@ -134,10 +138,10 @@ export default class Client { status_code: response.status, url, }); - }; + } - doPut = async (url, body, headers = {}) => { - headers['X-Timezone-Offset'] = new Date().getTimezoneOffset(); + private async doPut(url: string, body: Object, headers: { [x: string]: string; } = {}): Promise { + headers['X-Timezone-Offset'] = String(new Date().getTimezoneOffset()); const options = { method: 'put', @@ -158,5 +162,5 @@ export default class Client { status_code: response.status, url, }); - }; + } } diff --git a/webapp/src/client/index.js b/webapp/src/client/index.ts similarity index 60% rename from webapp/src/client/index.js rename to webapp/src/client/index.ts index 9406f34d..eb60dfb2 100644 --- a/webapp/src/client/index.js +++ b/webapp/src/client/index.ts @@ -1,4 +1,4 @@ -import ClientClass from './client.js'; +import ClientClass from './client'; const Client = new ClientClass(); diff --git a/webapp/src/components/gitlab_project_selector/index.tsx b/webapp/src/components/gitlab_project_selector/index.tsx index 52ae01fb..6eeb58fc 100644 --- a/webapp/src/components/gitlab_project_selector/index.tsx +++ b/webapp/src/components/gitlab_project_selector/index.tsx @@ -7,8 +7,7 @@ import {Theme} from 'mattermost-redux/types/preferences'; import {getProjects} from 'src/actions'; import ReactSelectSetting from 'src/components/react_select_setting'; -import {GlobalState} from 'src/types/global_state'; -import {getPluginState} from 'src/selectors'; +import {getYourProjects} from 'src/selectors'; import {getErrorMessage} from 'src/utils/user_utils'; import {Project, ProjectSelection} from 'src/types/gitlab_types'; import {ErrorType, SelectionType} from 'src/types/common'; @@ -26,7 +25,7 @@ const GitlabProjectSelector = ({theme, required, onChange, value, addValidate, r const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(''); - const myProjects = useSelector((state: GlobalState) => getPluginState(state).yourProjects); + const myProjects = useSelector(getYourProjects); const dispatch = useDispatch(); diff --git a/webapp/src/components/modals/create_issue/create_issue_form.tsx b/webapp/src/components/modals/create_issue/create_issue_form.tsx index 7a437fd0..78a65a18 100644 --- a/webapp/src/components/modals/create_issue/create_issue_form.tsx +++ b/webapp/src/components/modals/create_issue/create_issue_form.tsx @@ -43,7 +43,7 @@ const CreateIssueForm = ({theme, handleClose, isSubmitting, setIsSubmitting}: Pr if (post) { setIssueDescription(post.message); } else if (channelId) { - setIssueTitle(title.substring(0, MAX_TITLE_LENGTH)); + setIssueTitle(title?.substring(0, MAX_TITLE_LENGTH) ?? ''); } }, []); @@ -68,7 +68,7 @@ const CreateIssueForm = ({theme, handleClose, isSubmitting, setIsSubmitting}: Pr assignees: assignees.map((assignee) => assignee.value), milestone: milestone?.value, post_id: postId, - channel_id: channelId, + channel_id: channelId as string, }; setIsSubmitting(true); @@ -186,7 +186,7 @@ const CreateIssueForm = ({theme, handleClose, isSubmitting, setIsSubmitting}: Pr required={true} disabled={false} maxLength={MAX_TITLE_LENGTH} - value={issueTitle} + value={issueTitle ?? ''} onChange={handleIssueTitleChange} /> {issueTitleValidationError} diff --git a/webapp/src/components/post_options/attach_comment_to_issue.tsx b/webapp/src/components/post_options/attach_comment_to_issue.tsx index 28798652..fff3cb0a 100644 --- a/webapp/src/components/post_options/attach_comment_to_issue.tsx +++ b/webapp/src/components/post_options/attach_comment_to_issue.tsx @@ -7,11 +7,10 @@ import {useDispatch, useSelector} from 'react-redux'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {isSystemMessage} from 'mattermost-redux/utils/post_utils'; -import manifest from 'src/manifest'; import GitLabIcon from 'src/images/icons/gitlab'; import {openAttachCommentToIssueModal} from 'src/actions'; -import {GlobalState} from 'src/types/global_state'; -import {isUserConnectedToGitlab} from 'src/selectors'; +import {getConnected} from 'src/selectors'; +import {GlobalState} from 'src/types/store'; interface PropTypes { postId: string; @@ -23,7 +22,7 @@ const AttachCommentToIssuePostMenuAction = ({postId}: PropTypes) => { const isPostSystemMessage = Boolean(!post || isSystemMessage(post)); return { - show: isUserConnectedToGitlab(state) && !isPostSystemMessage, + show: getConnected(state) && !isPostSystemMessage, }; }); diff --git a/webapp/src/components/post_options/create_issue.tsx b/webapp/src/components/post_options/create_issue.tsx index 12370150..80639570 100644 --- a/webapp/src/components/post_options/create_issue.tsx +++ b/webapp/src/components/post_options/create_issue.tsx @@ -7,10 +7,9 @@ import {getPost} from 'mattermost-redux/selectors/entities/posts'; import {isSystemMessage} from 'mattermost-redux/utils/post_utils'; import GitLabIcon from 'src/images/icons/gitlab'; -import manifest from 'src/manifest'; import {openCreateIssueModal} from 'src/actions'; -import {GlobalState} from 'src/types/global_state'; -import {isUserConnectedToGitlab} from 'src/selectors'; +import {getConnected} from 'src/selectors'; +import {GlobalState} from 'src/types/store'; type PropTypes = { postId: string; @@ -22,7 +21,7 @@ const CreateIssuePostMenuAction = ({postId}: PropTypes) => { const isPostSystemMessage = Boolean(!post || isSystemMessage(post)); return { - show: isUserConnectedToGitlab(state) && !isPostSystemMessage, + show: getConnected(state) && !isPostSystemMessage, }; }); diff --git a/webapp/src/components/rhs_sidebar/rhs_sidebar.css b/webapp/src/components/rhs_sidebar/rhs_sidebar.css index e802bf6b..09ff5b8f 100644 --- a/webapp/src/components/rhs_sidebar/rhs_sidebar.css +++ b/webapp/src/components/rhs_sidebar/rhs_sidebar.css @@ -88,13 +88,13 @@ .gitlab-rhs-Description { font-size: 12px; font-weight: 400; - letterSpacing: 0; + letter-spacing: 0; } .gitlab-rhs-Username { font-size: 11px; font-weight: 600; - letterSpacing: 0.02em; + letter-spacing: 0.02em; } .gitlab-rhs-GitLabURL { @@ -129,12 +129,12 @@ } .gitlab-rhs-SubscriptionHeader { - font-family: 'Open Sans'; + font-family: 'Open Sans' !important; font-size: 12px; font-weight: 600; line-height: 16px; - letterSpacing: 0.02em; - textTransform: uppercase; + letter-spacing: 0.02em; + text-transform: uppercase; margin: 0 0 4px 0; color: rgba(var(--center-channel-color-rgb), 0.72); } diff --git a/webapp/src/components/user_attribute/index.js b/webapp/src/components/user_attribute/index.js deleted file mode 100644 index 10758043..00000000 --- a/webapp/src/components/user_attribute/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import {connect} from 'react-redux'; -import {bindActionCreators} from 'redux'; - -import {getGitlabUser} from '../../actions'; - -import manifest from '../../manifest'; - -import UserAttribute from './user_attribute.jsx'; - -function mapStateToProps(state, ownProps) { - const idUser = ownProps.user ? ownProps.user.id : ''; - const {id} = manifest; - const user = state[`plugins-${id}`].gitlabUsers[idUser] || {}; - return { - id: idUser, - username: user.username, - gitlabURL: state[`plugins-${id}`].gitlabURL, - }; -} - -function mapDispatchToProps(dispatch) { - return { - actions: bindActionCreators( - { - getGitlabUser, - }, - dispatch, - ), - }; -} - -export default connect( - mapStateToProps, - mapDispatchToProps, -)(UserAttribute); diff --git a/webapp/src/components/user_attribute/index.ts b/webapp/src/components/user_attribute/index.ts new file mode 100644 index 00000000..edfe963a --- /dev/null +++ b/webapp/src/components/user_attribute/index.ts @@ -0,0 +1,41 @@ +import {connect} from 'react-redux'; +import {AnyAction, Dispatch, bindActionCreators} from 'redux'; + +import {UserProfile} from 'mattermost-redux/types/users'; + +import {getGitlabUser} from '../../actions'; + +import {GlobalState, pluginStateKey} from 'src/types/store'; + +import UserAttribute from './user_attribute'; + +export type UserAttributeProps = UserAttributeStateProps & UserAttributeDispatchProps + +type UserAttributeDispatchProps = ReturnType; +type UserAttributeStateProps = ReturnType; + +function mapStateToProps(state: GlobalState, ownProps: {user: UserProfile}) { + const idUser = ownProps.user ? ownProps.user.id : ''; + const user = state[pluginStateKey].gitlabUsers[idUser] || {}; + return { + id: idUser, + username: user.username, + gitlabURL: state[pluginStateKey].gitlabURL, + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + actions: bindActionCreators( + { + getGitlabUser, + }, + dispatch, + ), + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(UserAttribute); diff --git a/webapp/src/components/user_attribute/user_attribute.jsx b/webapp/src/components/user_attribute/user_attribute.jsx deleted file mode 100644 index 2f1d2652..00000000 --- a/webapp/src/components/user_attribute/user_attribute.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -export default class UserAttribute extends React.PureComponent { - static propTypes = { - id: PropTypes.string.isRequired, - username: PropTypes.string, - gitlabURL: PropTypes.string, - actions: PropTypes.shape({ - getGitlabUser: PropTypes.func.isRequired, - }).isRequired, - }; - - constructor(props) { - super(props); - props.actions.getGitlabUser(props.id); - } - - render() { - const username = this.props.username; - const baseURL = this.props.gitlabURL; - - if (!username || !baseURL) { - return null; - } - - return ( - - ); - } -} - -const style = { - container: { - margin: '5px 0', - }, -}; diff --git a/webapp/src/components/user_attribute/user_attribute.tsx b/webapp/src/components/user_attribute/user_attribute.tsx new file mode 100644 index 00000000..c53b2d03 --- /dev/null +++ b/webapp/src/components/user_attribute/user_attribute.tsx @@ -0,0 +1,40 @@ +import React, {useEffect, CSSProperties} from 'react'; + +interface UserAttributeProps { + id: string; + username?: string; + gitlabURL?: string; + actions: { + getGitlabUser: (id: string) => void; + }; +} + +const UserAttribute = ({id, username, gitlabURL, actions}: UserAttributeProps) => { + useEffect(() => { + actions.getGitlabUser(id); + }, [id, actions]); + + if (!username || !gitlabURL) { + return null; + } + + return ( + + ); +}; + +const style: {container: CSSProperties} = { + container: { + margin: '5px 0', + }, +}; + +export default UserAttribute; diff --git a/webapp/src/index.ts b/webapp/src/index.ts index 97f99efb..9e25829c 100644 --- a/webapp/src/index.ts +++ b/webapp/src/index.ts @@ -2,9 +2,6 @@ // See License.txt for license information. import {getConfig} from 'mattermost-redux/selectors/entities/general'; - -import {GlobalState} from 'mattermost-redux/types/store'; - import {Store, Action} from 'redux'; import SidebarHeader from './components/sidebar_header'; @@ -18,6 +15,8 @@ import AttachCommentToIssueModal from './components/modals/attach_comment_to_iss import SidebarRight from './components/sidebar_right'; import LinkTooltip from './components/link_tooltip'; +import {GlobalState} from './types/store'; + import Reducer from './reducers'; import {getConnected, setShowRHSAction} from './actions'; import { diff --git a/webapp/src/reducers/index.js b/webapp/src/reducers/index.ts similarity index 63% rename from webapp/src/reducers/index.js rename to webapp/src/reducers/index.ts index 8df9eb22..28679693 100644 --- a/webapp/src/reducers/index.js +++ b/webapp/src/reducers/index.ts @@ -1,18 +1,21 @@ -import {combineReducers} from 'redux'; +import {combineReducers, Reducer} from 'redux'; import ActionTypes from '../action_types'; import Constants from '../constants'; +import {Item} from 'src/types/gitlab_items'; +import {ConnectedData, CreateIssueModalData, GitlabUsersData, LHSData, ShowRhsPluginActionData, SubscriptionData} from 'src/types'; +import {Project} from 'src/types/gitlab_types'; -function connected(state = false, action) { +const connected: Reducer = (state = false, action) => { switch (action.type) { case ActionTypes.RECEIVED_CONNECTED: return action.data.connected; default: return state; } -} +}; -function gitlabURL(state = '', action) { +const gitlabURL: Reducer = (state = '', action) => { switch (action.type) { case ActionTypes.RECEIVED_CONNECTED: if (action.data && action.data.gitlab_url) { @@ -22,9 +25,9 @@ function gitlabURL(state = '', action) { default: return state; } -} +}; -function organization(state = '', action) { +const organization: Reducer = (state = '', action) => { switch (action.type) { case ActionTypes.RECEIVED_CONNECTED: if (action.data && action.data.organization) { @@ -34,79 +37,71 @@ function organization(state = '', action) { default: return state; } -} +}; -function username(state = '', action) { +const username: Reducer = (state = '', action) => { switch (action.type) { case ActionTypes.RECEIVED_CONNECTED: return action.data.gitlab_username; default: return state; } -} +}; -function settings( - state = { - sidebar_buttons: Constants.SETTING_BUTTONS_TEAM, - daily_reminder: true, - notifications: true, - }, - action, -) { +const settings: Reducer<{ + sidebar_buttons: string, + daily_reminder: boolean, + notifications: boolean, +}, {type: string, data: ConnectedData}> = (state = { + sidebar_buttons: Constants.SETTING_BUTTONS_TEAM, + daily_reminder: true, + notifications: true, +}, action) => { switch (action.type) { case ActionTypes.RECEIVED_CONNECTED: return action.data.settings; default: return state; } -} +}; -function clientId(state = '', action) { +const clientId: Reducer = (state = '', action) => { switch (action.type) { case ActionTypes.RECEIVED_CONNECTED: return action.data.gitlab_client_id; default: return state; } -} +}; -function reviewDetails(state = [], action) { +const reviewDetails: Reducer = (state = [], action) => { switch (action.type) { case ActionTypes.RECEIVED_REVIEW_DETAILS: return action.data; default: return state; } -} +}; -function yourPrDetails(state = [], action) { +const yourPrDetails: Reducer = (state = [], action) => { switch (action.type) { case ActionTypes.RECEIVED_YOUR_PR_DETAILS: return action.data; default: return state; } -} +}; -function lhsData(state = [], action) { +const lhsData: Reducer = (state = null, action) => { switch (action.type) { case ActionTypes.RECEIVED_LHS_DATA: return action.data; default: return state; } -} - -function mentions(state = [], action) { - switch (action.type) { - case ActionTypes.RECEIVED_MENTIONS: - return action.data; - default: - return state; - } -} +}; -function rhsPluginAction(state = null, action) { +function rhsPluginAction(state = null, action: {type: string, showRHSPluginAction: ShowRhsPluginActionData}) { switch (action.type) { case ActionTypes.RECEIVED_SHOW_RHS_ACTION: return action.showRHSPluginAction; @@ -115,16 +110,16 @@ function rhsPluginAction(state = null, action) { } } -function rhsState(state = null, action) { +const rhsState: Reducer = (state = null, action) => { switch (action.type) { case ActionTypes.UPDATE_RHS_STATE: return action.state; default: return state; } -} +}; -function gitlabUsers(state = {}, action) { +const gitlabUsers: Reducer, {type: string, data: GitlabUsersData, userID: string}> = (state = {}, action) => { switch (action.type) { case ActionTypes.RECEIVED_GITLAB_USER: { const nextState = {...state}; @@ -134,9 +129,9 @@ function gitlabUsers(state = {}, action) { default: return state; } -} +}; -const isCreateIssueModalVisible = (state = false, action) => { +const isCreateIssueModalVisible = (state = false, action: {type: string}) => { switch (action.type) { case ActionTypes.OPEN_CREATE_ISSUE_MODAL: case ActionTypes.OPEN_CREATE_ISSUE_MODAL_WITHOUT_POST: @@ -148,7 +143,7 @@ const isCreateIssueModalVisible = (state = false, action) => { } }; -const isAttachCommentToIssueModalVisible = (state = false, action) => { +const isAttachCommentToIssueModalVisible = (state = false, action: {type: string}) => { switch (action.type) { case ActionTypes.OPEN_ATTACH_COMMENT_TO_ISSUE_MODAL: return true; @@ -159,7 +154,7 @@ const isAttachCommentToIssueModalVisible = (state = false, action) => { } }; -const postIdForAttachCommentToIssueModal = (state = {}, action) => { +const postIdForAttachCommentToIssueModal = (state = '', action: {type: string, data: {postId: string}}) => { switch (action.type) { case ActionTypes.OPEN_ATTACH_COMMENT_TO_ISSUE_MODAL: return action.data.postId; @@ -170,12 +165,13 @@ const postIdForAttachCommentToIssueModal = (state = {}, action) => { } }; -const createIssueModal = (state = {}, action) => { +const createIssueModal: Reducer = (state = {}, action) => { switch (action.type) { case ActionTypes.OPEN_CREATE_ISSUE_MODAL: case ActionTypes.OPEN_CREATE_ISSUE_MODAL_WITHOUT_POST: return { ...state, + postId: action.data.postId, title: action.data.title, channelId: action.data.channelId, @@ -187,7 +183,7 @@ const createIssueModal = (state = {}, action) => { } }; -function yourProjects(state = [], action) { +function yourProjects(state = [] as Project[], action: {type: string, data: Project[]}) { switch (action.type) { case ActionTypes.RECEIVED_PROJECTS: return action.data; @@ -196,7 +192,7 @@ function yourProjects(state = [], action) { } } -function subscriptions(state = {}, action) { +const subscriptions: Reducer, {type: string, data: {channelId: string, subscriptions: SubscriptionData}}> = (state = {}, action) => { switch (action.type) { case ActionTypes.RECEIVED_CHANNEL_SUBSCRIPTIONS: { const nextState = {...state}; @@ -207,7 +203,7 @@ function subscriptions(state = {}, action) { default: return state; } -} +}; export default combineReducers({ connected, @@ -216,7 +212,6 @@ export default combineReducers({ username, settings, clientId, - mentions, gitlabUsers, isCreateIssueModalVisible, yourProjects, diff --git a/webapp/src/selectors/index.js b/webapp/src/selectors/index.ts similarity index 52% rename from webapp/src/selectors/index.js rename to webapp/src/selectors/index.ts index 3fcbfcf4..e09f80aa 100644 --- a/webapp/src/selectors/index.js +++ b/webapp/src/selectors/index.ts @@ -5,8 +5,11 @@ import {createSelector} from 'reselect'; import {getPost} from 'mattermost-redux/selectors/entities/posts'; import manifest from '../manifest'; +import {Item} from 'src/types/gitlab_items'; +import {GlobalState, PluginState, pluginStateKey} from 'src/types/store'; +import {SideBarData} from 'src/types'; -export const getPluginServerRoute = (state) => { +export const getPluginServerRoute = (state: GlobalState) => { const config = getConfig(state); let basePath = ''; @@ -21,7 +24,7 @@ export const getPluginServerRoute = (state) => { return basePath + '/plugins/' + manifest.id; }; -function mapPrsToDetails(prs, details) { +function mapPrsToDetails(prs?: Item[], details?: Item[]): Item[] { if (!prs || !prs.length) { return []; } @@ -41,34 +44,32 @@ function mapPrsToDetails(prs, details) { }); } -export const getPluginState = (state) => state[`plugins-${manifest.id}`]; - -export const isUserConnectedToGitlab = (state) => state[`plugins-${manifest.id}`].connected; +export const getPluginState = (state: GlobalState) => state[pluginStateKey]; export const getSidebarData = createSelector( getPluginState, - (pluginState) => { + (pluginState: PluginState): SideBarData => { return { username: pluginState.username, - reviewDetails: pluginState.reviewDetails, - reviews: mapPrsToDetails(pluginState.lhsData?.reviews, pluginState.reviewDetails), - yourAssignedPrs: mapPrsToDetails(pluginState.lhsData?.yourAssignedPrs, pluginState.yourPrDetails), - yourPrDetails: pluginState.yourPrDetails, - yourAssignedIssues: pluginState.lhsData?.yourAssignedIssues, - todos: pluginState.lhsData?.todos, + reviewDetails: pluginState.reviewDetails ?? [], + reviews: mapPrsToDetails(pluginState.lhsData?.reviews, pluginState.reviewDetails || []), + yourAssignedPrs: mapPrsToDetails(pluginState.lhsData?.yourAssignedPrs, pluginState.yourPrDetails || []), + yourPrDetails: pluginState.yourPrDetails ?? [], + yourAssignedIssues: pluginState.lhsData?.yourAssignedIssues ?? [], + todos: pluginState.lhsData?.todos ?? [], org: pluginState.organization, gitlabURL: pluginState.gitlabURL, - rhsState: pluginState.rhsState, + rhsState: pluginState.rhsState ?? '', }; }, ); -export const isCreateIssueModalVisible = (state) => state[`plugins-${manifest.id}`].isCreateIssueModalVisible; +export const isCreateIssueModalVisible = (state: GlobalState) => getPluginState(state).isCreateIssueModalVisible; -export const isAttachCommentToIssueModalVisible = (state) => state[`plugins-${manifest.id}`].isAttachCommentToIssueModalVisible; +export const isAttachCommentToIssueModalVisible = (state: GlobalState) => getPluginState(state).isAttachCommentToIssueModalVisible; -export const getCreateIssueModalContents = (state) => { - const {postId, title, channelId} = state[`plugins-${manifest.id}`].createIssueModal; +export const getCreateIssueModalContents = (state: GlobalState) => { + const {postId, title, channelId} = getPluginState(state).createIssueModal; const post = postId ? getPost(state, postId) : null; return { @@ -78,13 +79,15 @@ export const getCreateIssueModalContents = (state) => { }; }; -export const getAttachCommentModalContents = (state) => { - const postId = state[`plugins-${manifest.id}`].postIdForAttachCommentToIssueModal; +export const getAttachCommentModalContents = (state: GlobalState) => { + const postId = getPluginState(state).postIdForAttachCommentToIssueModal; const post = getPost(state, postId); return post; }; -export const getConnected = (state) => state[`plugins-${manifest.id}`].connected; +export const getYourProjects = (state: GlobalState) => getPluginState(state).yourProjects; + +export const getConnected = (state: GlobalState) => getPluginState(state).connected; -export const getConnectedGitlabUrl = (state) => state[`plugins-${manifest.id}`].gitlabURL; +export const getConnectedGitlabUrl = (state: GlobalState) => getPluginState(state).gitlabURL; diff --git a/webapp/src/types/gitlab_items.ts b/webapp/src/types/gitlab_items.ts index 0de7971d..b7ce5b4b 100644 --- a/webapp/src/types/gitlab_items.ts +++ b/webapp/src/types/gitlab_items.ts @@ -72,6 +72,7 @@ export interface GitlabItemsProps { } export interface TooltipData { + owner: string; state: string; type: string; repo: string; diff --git a/webapp/src/types/global_state.ts b/webapp/src/types/global_state.ts deleted file mode 100644 index 606f1f7d..00000000 --- a/webapp/src/types/global_state.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {GlobalState as ReduxGlobalState} from 'mattermost-redux/types/store'; - -import {Project} from './gitlab_types'; - -export type GlobalState = ReduxGlobalState & { - 'plugins-com.github.manland.mattermost-plugin-gitlab': { - createIssueModal: { - postId: string; - title: string; - channelId: string; - }; - isCreateIssueModalVisible: boolean; - yourProjects: Project[]; - connected: boolean; - postIdForAttachCommentToIssueModal: string; - isAttachCommentToIssueModalVisible: boolean; - } -} diff --git a/webapp/src/types/index.ts b/webapp/src/types/index.ts new file mode 100644 index 00000000..6ed5236e --- /dev/null +++ b/webapp/src/types/index.ts @@ -0,0 +1,66 @@ +import {Item} from './gitlab_items'; + +export type ConnectedData = { + gitlab_url: string; + connected: boolean; + organization: string; + gitlab_username: string; + settings: UserSettingsData; + gitlab_client_id: string; +} + +export type LHSData = { + reviews: Item[]; + yourAssignedPrs: Item[]; + yourAssignedIssues: Item[]; + todos: Item[]; +} + +export type UserSettingsData = { + sidebar_buttons: string; + daily_reminder: boolean; + notifications: boolean; +} + +export type GitlabUsersData = { + username: string; + last_try: number; +} + +export type SubscriptionData = { + repository_url: string; + repository_name: string; + features: string[]; + creator_id: string; +} + +export type ShowRhsPluginActionData = { + type: string; + state: string; + pluggableId: string; +} + +export type APIError = { + id?: string; + message: string; + status: number; +} + +export type SideBarData = { + username: string; + reviewDetails: Item[]; + reviews: Item[]; + yourAssignedPrs: Item[]; + yourPrDetails: Item[]; + yourAssignedIssues: Item[]; + todos: Item[]; + org: string; + gitlabURL: string; + rhsState: string; +} + +export type CreateIssueModalData = { + postId?: string; + title?: string; + channelId?: string; +} diff --git a/webapp/src/types/store.ts b/webapp/src/types/store.ts new file mode 100644 index 00000000..573114b6 --- /dev/null +++ b/webapp/src/types/store.ts @@ -0,0 +1,11 @@ +import {GlobalState as ReduxGlobalState} from 'mattermost-redux/types/store'; + +import type combinedReducers from '../reducers'; + +export type GlobalState = ReduxGlobalState & { + 'plugins-com.github.manland.mattermost-plugin-gitlab': PluginState +}; + +export type PluginState = ReturnType + +export const pluginStateKey = 'plugins-com.github.manland.mattermost-plugin-gitlab' as const;