diff --git a/README.md b/README.md index 90ab5fd..6390a67 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ ## How to use 1. Install the plugin -1. Open a repo host by `coding.net` +1. Open a repo hosted by `coding.net` 1. Click on `Log in` button -1. You will be redirected back to the editor, free to try & open an issue +1. You will be redirected back to the editor, feel free to try & open an issue ## Development diff --git a/package.json b/package.json index 004772d..55e9d93 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "coding-plugin", "displayName": "CODING Merge Requests & Releases", "description": "CODING plugin for VS Code.", - "version": "0.2.1", + "version": "0.3.0", "publisher": "coding-net", "license": "MIT", "engines": { @@ -19,7 +19,7 @@ "icon": "assets/coding.png", "repository": { "type": "git", - "url": "https://github.com/cangzhang/coding-vscode.git" + "url": "https://github.com/Coding/coding-vscode.git" }, "main": "./out/extension.js", "contributes": { diff --git a/src/codingServer.ts b/src/codingServer.ts index 543530b..e259c0b 100644 --- a/src/codingServer.ts +++ b/src/codingServer.ts @@ -17,6 +17,7 @@ import { IMemberListResp, IMRContentResp, ICreateCommentResp, + IMRStatusResp, } from 'src/typings/respResult'; import { PromiseAdapter, promiseFromEvent, parseQuery, parseCloneUrl } from 'src/common/utils'; import { GitService } from 'src/common/gitService'; @@ -96,6 +97,13 @@ export class CodingServer { await vscode.commands.executeCommand('setContext', 'hasRepo', !!repoInfo?.repo); const result = await this.getUserInfo(repoInfo?.team || ``, accessToken); const { data: userInfo } = result; + + if (userInfo.team !== repoInfo?.team) { + this._loggedIn = false; + await vscode.commands.executeCommand('setContext', 'loggedIn', this._loggedIn); + throw new Error(`team not match`); + } + const ret: ISessionData = { id: nanoid(), user: userInfo, @@ -686,6 +694,27 @@ export class CodingServer { } } + public async fetchMRStatus(iid: string) { + try { + const { repoApiPrefix } = await this.getApiPrefix(); + const resp: IMRStatusResp = await got + .get(`${repoApiPrefix}/merge/${iid}/commit-statuses`, { + searchParams: { + access_token: this._session?.accessToken, + }, + }) + .json(); + + if (resp.code) { + return Promise.reject(resp); + } + + return resp; + } catch (e) { + return Promise.reject(e); + } + } + get loggedIn() { return this._loggedIn; } diff --git a/src/panel.ts b/src/panel.ts index f48bddf..8b3d98c 100644 --- a/src/panel.ts +++ b/src/panel.ts @@ -106,12 +106,17 @@ export class Panel { break; case 'mr.update.reviewers': { try { - const { iid, list: selected }: { iid: string; list: number[] } = args; + const { + iid, + list: selected, + author, + }: { iid: string; list: number[]; author: string } = args; const { data: { list: memberList }, } = await this._codingSrv.getProjectMembers(); + const list = memberList - .filter((i) => i.user.global_key !== this._codingSrv.session?.user?.global_key) + .filter((i) => i.user.global_key !== author) .map((i) => ({ label: i.user.name, description: i.user.global_key, @@ -152,6 +157,14 @@ export class Panel { } catch (e) {} break; } + case `mr.fetch.status`: { + try { + const { iid } = args; + const resp = await this._codingSrv.fetchMRStatus(iid); + this._replyMessage(message, resp.data); + } catch (e) {} + break; + } default: return this.MESSAGE_UNHANDLED; } diff --git a/src/tree/mrTree.ts b/src/tree/mrTree.ts index 3d9c91a..9e42dee 100644 --- a/src/tree/mrTree.ts +++ b/src/tree/mrTree.ts @@ -20,6 +20,8 @@ enum ItemType { Node = `node`, } +const capitalized = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); + const getIcon = (name: string, theme: string) => path.join(__filename, `../../../assets/${theme}/${name}.png`); @@ -58,7 +60,7 @@ export class MRTreeDataProvider implements vscode.TreeDataProvider): Thenable[]> { if (!this._service.loggedIn) { - vscode.window.showErrorMessage(`[MR Tree] Invalid credentials.`); + console.error(`[MR Tree] Invalid credentials.`); return Promise.resolve([]); } @@ -94,17 +96,13 @@ export class MRTreeDataProvider implements vscode.TreeDataProvider { if (resp.code) { const msg = Object.values(resp.msg || {})[0]; - vscode.window.showErrorMessage(`[MR] list: ${msg}`); + console.error(`[MR] list: ${msg}`); return []; } diff --git a/src/typings/commonTypes.ts b/src/typings/commonTypes.ts index 02edaff..7ba3245 100644 --- a/src/typings/commonTypes.ts +++ b/src/typings/commonTypes.ts @@ -1,4 +1,4 @@ -import { IMRDetail, IUserItem } from './respResult'; +import { IMRDetail, IMRStatusItem, IUserItem } from './respResult'; export interface IRepoInfo { team: string; @@ -37,6 +37,7 @@ export interface IMRWebViewDetail { data: IMRDetail & { loading: boolean; editingDesc: boolean; + commit_statuses: IMRStatusItem[]; }; user: IUserItem; } diff --git a/src/typings/respResult.ts b/src/typings/respResult.ts index 69d14f4..abe2816 100644 --- a/src/typings/respResult.ts +++ b/src/typings/respResult.ts @@ -244,3 +244,25 @@ export interface IMRContentResp extends CodingResponse { export interface ICreateCommentResp extends CodingResponse { data: IComment; } + +export interface IMRStatusItem { + state: string; + sha: string; + context: string; + target_url: string; + description: string; + ignore: boolean; +} + +export interface IMRStatus { + state: string; + pendingStateCount: number; + successStateCount: number; + failureStateCount: number; + errorStateCount: number; + statuses: IMRStatusItem[]; +} + +export interface IMRStatusResp extends CodingResponse { + data: IMRStatus; +} diff --git a/webviews/App.tsx b/webviews/App.tsx index f199f0d..059a471 100644 --- a/webviews/App.tsx +++ b/webviews/App.tsx @@ -1,13 +1,16 @@ -import React, { FormEvent, useRef, useState } from 'react'; +import React, { FormEvent, useEffect, useRef, useState, useCallback } from 'react'; import { view } from '@risingstack/react-easy-state'; +import { IMRStatus } from 'src/typings/respResult'; + import appStore from 'webviews/store/appStore'; import persistDataHook from 'webviews/hooks/persistDataHook'; -import Activities from 'webviews/components/Activities'; -import Reviewers from 'webviews/components/Reviewers'; import initDataHook from 'webviews/hooks/initDataHook'; -import EditButton from 'webviews/components/EditButton'; -// import { requestUpdateMRContent } from 'webviews/service/mrService'; + +import Activities from 'webviews/components/mr/Activities'; +import Reviewers from 'webviews/components/mr/Reviewers'; +import EditButton from 'webviews/components/mr/EditButton'; +import StatusCheck from 'webviews/components/mr/StatusCheck'; import { EmptyWrapper, @@ -25,11 +28,13 @@ import { } from 'webviews/app.styles'; function App() { - const { currentMR, updateMRTitle, toggleUpdatingDesc, updateMRDesc } = appStore; + const { currentMR, updateMRTitle, toggleUpdatingDesc, updateMRDesc, fetchMRStatus } = appStore; const [isEditingTitle, setEditingTitle] = useState(false); const [title, setTitle] = useState(currentMR?.data?.merge_request?.title); const inputRef = useRef(null); const [desc, setDesc] = useState(``); + const statusChecker = useRef(); + const [statusData, setStatusData] = useState(null); const { repoInfo, data } = currentMR; const { merge_request: mergeRequest } = data || {}; @@ -37,6 +42,32 @@ function App() { persistDataHook(); initDataHook(); + useEffect(() => { + statusChecker.current = window.setTimeout(async () => { + try { + await onRefreshStatus(); + } finally { + window.clearTimeout(statusChecker.current); + + statusChecker.current = window.setInterval(async () => { + await onRefreshStatus(); + }, 30 * 1000); + } + }, 3 * 1000); + + return () => { + window.clearTimeout(statusChecker.current); + window.clearInterval(statusChecker.current); + setStatusData(null); + }; + }, [currentMR.iid]); + + const onRefreshStatus = useCallback(async () => { + const resp = await fetchMRStatus(currentMR.iid); + setStatusData(resp); + return null; + }, [currentMR.iid]); + const handleKeyDown = async (event: any) => { if (event.key === 'Enter') { await updateMRTitle(title); @@ -146,6 +177,9 @@ function App() { onChange={onChangeDesc} /> )} + + + Activities diff --git a/webviews/assets/edit.svg b/webviews/assets/edit.svg index b02c84f..9047168 100644 --- a/webviews/assets/edit.svg +++ b/webviews/assets/edit.svg @@ -1,4 +1,4 @@ - + diff --git a/webviews/assets/refresh.svg b/webviews/assets/refresh.svg new file mode 100644 index 0000000..3ad3c0f --- /dev/null +++ b/webviews/assets/refresh.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/webviews/components/EditButton.tsx b/webviews/components/EditButton.tsx deleted file mode 100644 index e3e02cd..0000000 --- a/webviews/components/EditButton.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -import EditIcon from 'webviews/assets/edit.svg'; - -const IconButton = styled.button` - border: unset; - background: unset; - width: 20px; - height: 20px; - margin-left: 1ex; - padding: 2px 0; - vertical-align: middle; - - :hover { - cursor: pointer; - } - - :focus { - outline: 1px solid var(--vscode-focusBorder); - outline-offset: 2px; - } - - svg path { - fill: var(--vscode-foreground); - } -`; - -interface Props { - onClick?: () => void; -} - -const EditButton = ({ onClick = () => null }: Props) => { - return ( - - - - ); -}; - -export default EditButton; diff --git a/webviews/components/IconButton.tsx b/webviews/components/IconButton.tsx new file mode 100644 index 0000000..103d9f6 --- /dev/null +++ b/webviews/components/IconButton.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import styled, { css, keyframes } from 'styled-components'; + +interface Props { + onClick?: () => void; + children: React.ReactElement; + title?: string; + width?: number; + height?: number; + rotate?: boolean; +} + +const rotate = keyframes` + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +`; + +const Button = styled(({ height, width, rotate, ...rest }: Props) => + ); +}; + +export default IconButton; diff --git a/webviews/components/Activities.tsx b/webviews/components/mr/Activities.tsx similarity index 100% rename from webviews/components/Activities.tsx rename to webviews/components/mr/Activities.tsx diff --git a/webviews/components/Activity.tsx b/webviews/components/mr/Activity.tsx similarity index 100% rename from webviews/components/Activity.tsx rename to webviews/components/mr/Activity.tsx diff --git a/webviews/components/AddComment.tsx b/webviews/components/mr/AddComment.tsx similarity index 96% rename from webviews/components/AddComment.tsx rename to webviews/components/mr/AddComment.tsx index 539fde2..0183a56 100644 --- a/webviews/components/AddComment.tsx +++ b/webviews/components/mr/AddComment.tsx @@ -36,14 +36,12 @@ function AddComment() { const showAllowMergeBtn = mrStatusOk && mergeRequest?.author?.id !== user?.id; const getAgreed = () => { - let agreed = true; const index = reviewers.reviewers.findIndex((r) => r.reviewer.id === user.id); - + let agreed = reviewers.volunteer_reviewers.findIndex((r) => r.reviewer.id === user.id) >= 0; if (index >= 0) { agreed = reviewers.reviewers[index].value === 100; - } else { - agreed = reviewers.volunteer_reviewers.findIndex((r) => r.reviewer.id === user.id) >= 0; } + return agreed; }; diff --git a/webviews/components/Comment.tsx b/webviews/components/mr/Comment.tsx similarity index 100% rename from webviews/components/Comment.tsx rename to webviews/components/mr/Comment.tsx diff --git a/webviews/components/mr/EditButton.tsx b/webviews/components/mr/EditButton.tsx new file mode 100644 index 0000000..7801eb4 --- /dev/null +++ b/webviews/components/mr/EditButton.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +import EditIcon from 'webviews/assets/edit.svg'; +import IconButton from 'webviews/components/IconButton'; + +interface Props { + onClick?: () => void; +} + +const EditButton = ({ onClick = () => null }: Props) => { + return ( + + + + ); +}; + +export default EditButton; diff --git a/webviews/components/Reviewers.tsx b/webviews/components/mr/Reviewers.tsx similarity index 86% rename from webviews/components/Reviewers.tsx rename to webviews/components/mr/Reviewers.tsx index 1e08a04..de28588 100644 --- a/webviews/components/Reviewers.tsx +++ b/webviews/components/mr/Reviewers.tsx @@ -3,8 +3,8 @@ import styled from 'styled-components'; import { view } from '@risingstack/react-easy-state'; import appStore from 'webviews/store/appStore'; -import { Avatar, AuthorLink } from 'webviews/components/User'; -import EditButton from 'webviews/components/EditButton'; +import { Avatar, AuthorLink } from 'webviews/components/mr/User'; +import EditButton from 'webviews/components/mr/EditButton'; const Title = styled.div` margin-top: 15px; @@ -32,7 +32,7 @@ function Reviewers() { const onUpdateReviewer = useCallback(() => { const list = allReviewers.map((i) => i.reviewer.id); - updateReviewers(currentMR.iid, list); + updateReviewers(currentMR.iid, list, currentMR.data.merge_request.author.global_key); }, [allReviewers]); return ( diff --git a/webviews/components/mr/StatusCheck.tsx b/webviews/components/mr/StatusCheck.tsx new file mode 100644 index 0000000..0af0393 --- /dev/null +++ b/webviews/components/mr/StatusCheck.tsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { sleep } from 'webviews/utils/helper'; +import { IMRStatus } from 'src/typings/respResult'; +import { SectionTitle } from 'webviews/app.styles'; +import RefreshIcon from 'webviews/assets/refresh.svg'; +import IconButton from 'webviews/components/IconButton'; + +interface Props { + data: IMRStatus | null; + onRefresh: (...args: any[]) => Promise; +} + +const ListItem = styled.li` + i { + margin-left: 2ex; + } +`; + +function StatusCheck(props: Props) { + const { data } = props; + const [refreshing, setRefreshing] = useState(false); + + const onRefresh = async () => { + if (refreshing) { + return; + } + + setRefreshing(true); + // minimum 1s + await Promise.allSettled([props.onRefresh, sleep(1000)]); + setRefreshing(false); + }; + + return ( + <> + + Status Check + + + + +
    + {data?.statuses ? ( + data?.statuses.map((i) => { + return ( + + {i.context} + + {i.description} + + + ); + }) + ) : ( +
  • No related job found
  • + )} +
+ + ); +} + +export default StatusCheck; diff --git a/webviews/components/User.tsx b/webviews/components/mr/User.tsx similarity index 100% rename from webviews/components/User.tsx rename to webviews/components/mr/User.tsx diff --git a/webviews/store/appStore.ts b/webviews/store/appStore.ts index 7a3bb49..2c399a4 100644 --- a/webviews/store/appStore.ts +++ b/webviews/store/appStore.ts @@ -106,10 +106,10 @@ const appStore = store({ appStore.comments.push([result] as any); return result; }, - async updateReviewers(iid: string, list: number[]) { + async updateReviewers(iid: string, list: number[], author: string) { const resp = await getMessageHandler(appStore.messageHandler)().postMessage({ command: actions.MR_UPDATE_REVIEWERS, - args: { iid, list }, + args: { iid, list, author }, }); appStore.reviewers = resp; appStore.refreshMRActivities(); @@ -146,6 +146,14 @@ const appStore = store({ initMRActivities(data: IActivity[]) { appStore.updateMRActivities(data); }, + async fetchMRStatus(iid: string) { + const resp = await getMessageHandler(appStore.messageHandler)().postMessage({ + command: actions.MR_FETCH_STATUS, + args: { iid }, + }); + + return resp; + }, messageHandler(message: any) { const { updateMRComments } = appStore; const { command, res } = message; diff --git a/webviews/store/constants.ts b/webviews/store/constants.ts index 9e07a16..e0b1c08 100644 --- a/webviews/store/constants.ts +++ b/webviews/store/constants.ts @@ -13,4 +13,5 @@ export enum actions { MR_UPDATE_DESC = `mr.update.desc`, MR_REVIEWERS_INIT = `mr.reviewers.init`, MR_ACTIVITIES_INIT = `mr.activities.init`, + MR_FETCH_STATUS = `mr.fetch.status`, } diff --git a/webviews/utils/helper.ts b/webviews/utils/helper.ts new file mode 100644 index 0000000..2ae5e32 --- /dev/null +++ b/webviews/utils/helper.ts @@ -0,0 +1,7 @@ +export function sleep(timeout: number) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(true); + }, timeout); + }); +} diff --git a/webviews/utils/message.ts b/webviews/utils/message.ts index cc4c636..f67f3f6 100644 --- a/webviews/utils/message.ts +++ b/webviews/utils/message.ts @@ -1,14 +1,15 @@ import { vscode } from 'webviews/constants/vscode'; +import { nanoid } from 'nanoid'; import { IRequestMessage, IReplyMessage } from 'src/typings/message'; export class MessageHandler { private _commandHandler: ((message: any) => void) | null; - private lastSentReq: number; + // private lastSentReq: string; private pendingReplies: any; constructor(commandHandler: any) { this._commandHandler = commandHandler; - this.lastSentReq = 0; + // this.lastSentReq = nanoid(); this.pendingReplies = Object.create(null); window.addEventListener('message', this.handleMessage.bind(this)); } @@ -18,7 +19,7 @@ export class MessageHandler { } public async postMessage(message: any): Promise { - const req = String(++this.lastSentReq); + const req = nanoid(); return new Promise((resolve, reject) => { this.pendingReplies[req] = { resolve: resolve,