├── .prettierignore ├── .vscode ├── settings.json ├── extensions.json ├── tasks.json └── launch.json ├── webviews ├── constants │ ├── vscode.ts │ ├── mergeRequest.ts │ └── activity.ts ├── utils │ ├── helper.ts │ ├── time.tsx │ └── message.ts ├── assets │ ├── plus.svg │ ├── check.svg │ ├── delete.svg │ ├── edit.svg │ └── refresh.svg ├── index.tsx ├── global.d.ts ├── components │ ├── mr │ │ ├── EditButton.tsx │ │ ├── User.tsx │ │ ├── Activities.tsx │ │ ├── Reviewers.tsx │ │ ├── StatusCheck.tsx │ │ ├── Comment.tsx │ │ ├── Activity.tsx │ │ └── AddComment.tsx │ └── IconButton.tsx ├── hooks │ ├── persistDataHook.tsx │ └── initDataHook.tsx ├── store │ ├── constants.ts │ └── appStore.ts ├── app.styles.tsx ├── index.css └── App.tsx ├── assets ├── coding.png ├── dark │ ├── icon_a.png │ ├── icon_d.png │ └── icon_m.png └── light │ ├── icon_a.png │ ├── icon_d.png │ └── icon_m.png ├── .vscodeignore ├── src ├── common │ ├── uri.ts │ ├── contants.ts │ ├── logger.ts │ ├── gitService.ts │ ├── utils.ts │ └── keychain.ts ├── utils │ └── error.ts ├── typings │ ├── message.ts │ ├── commonTypes.ts │ ├── respResult.ts │ └── git.d.ts ├── tsconfig.json ├── tree │ ├── releaseTree.ts │ ├── inMemMRContentProvider.ts │ └── mrTree.ts ├── reviewCommentController.ts ├── panel.ts ├── extension.ts └── codingServer.ts ├── .editorconfig ├── .gitignore ├── .prettierrc.js ├── README.md ├── .eslintrc.js ├── tsconfig.json ├── webpack.config.js └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | out/ 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false 3 | } 4 | -------------------------------------------------------------------------------- /webviews/constants/vscode.ts: -------------------------------------------------------------------------------- 1 | export const vscode = acquireVsCodeApi(); 2 | -------------------------------------------------------------------------------- /assets/coding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/coding.png -------------------------------------------------------------------------------- /assets/dark/icon_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/dark/icon_a.png -------------------------------------------------------------------------------- /assets/dark/icon_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/dark/icon_d.png -------------------------------------------------------------------------------- /assets/dark/icon_m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/dark/icon_m.png -------------------------------------------------------------------------------- /assets/light/icon_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/light/icon_a.png -------------------------------------------------------------------------------- /assets/light/icon_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/light/icon_d.png -------------------------------------------------------------------------------- /assets/light/icon_m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coding/coding-vscode/master/assets/light/icon_m.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | * 2 | */** 3 | **/*/.DS_Store 4 | !node_modules/**/* 5 | 6 | !out/**/* 7 | !assets/**/* 8 | 9 | !package.json 10 | !README.md 11 | -------------------------------------------------------------------------------- /src/common/uri.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | 3 | export const EMPTY_IMAGE_URI = Uri.parse(`data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==`); 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /src/utils/error.ts: -------------------------------------------------------------------------------- 1 | export const formatErrorMessage = (msg: string | Record) => { 2 | if (typeof msg === 'string') return msg; 3 | return Object.values(msg).join(); 4 | }; 5 | -------------------------------------------------------------------------------- /webviews/constants/mergeRequest.ts: -------------------------------------------------------------------------------- 1 | export const enum MERGE_STATUS { 2 | ACCEPTED = 'ACCEPTED', 3 | REFUSED = 'REFUSED', 4 | CANMERGE = 'CANMERGE', 5 | CANNOTMERGE = 'CANNOTMERGE', 6 | } 7 | -------------------------------------------------------------------------------- /webviews/utils/helper.ts: -------------------------------------------------------------------------------- 1 | export function sleep(timeout: number) { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(true); 5 | }, timeout); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | build 4 | web_modules/ 5 | node_modules/ 6 | .idea/ 7 | build/ 8 | out/ 9 | src/typings/vscode.d.ts 10 | src/typings/vscode.proposed.d.ts 11 | *.vsix 12 | -------------------------------------------------------------------------------- /src/common/contants.ts: -------------------------------------------------------------------------------- 1 | export const MRUriScheme = `coding-mr`; 2 | 3 | export const EmptyUserAvatar = `https://coding-net-production-static-ci.codehub.cn/7167f369-59ff-4196-bb76-a9959cf2b906.png`; 4 | -------------------------------------------------------------------------------- /webviews/assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/typings/message.ts: -------------------------------------------------------------------------------- 1 | export interface IRequestMessage { 2 | req: string; 3 | command: string; 4 | args: T; 5 | } 6 | 7 | export interface IReplyMessage { 8 | seq?: string; 9 | err?: any; 10 | res?: any; 11 | } 12 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | printWidth: 100, 4 | trailingComma: 'all', 5 | useTabs: false, 6 | semi: true, 7 | singleQuote: true, 8 | jsxSingleQuote: true, 9 | jsxBracketSameLine: true, 10 | }; 11 | -------------------------------------------------------------------------------- /webviews/utils/time.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dayjs from 'dayjs'; 3 | 4 | export const getTime = (time: number) => ( 5 | 8 | ); 9 | -------------------------------------------------------------------------------- /webviews/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | -------------------------------------------------------------------------------- /webviews/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /webviews/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const acquireVsCodeApi: any; 2 | declare module 'styled-components'; 3 | 4 | declare module '*.svg' { 5 | import * as React from 'react'; 6 | 7 | export const ReactComponent: React.FunctionComponent>; 8 | 9 | export default ReactComponent; 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["dbaeumer.vscode-eslint"] 7 | } 8 | -------------------------------------------------------------------------------- /webviews/assets/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["es2019", "dom", "es2020"], 6 | "outDir": "../out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": ".", 10 | "baseUrl": ".", 11 | "paths": { 12 | "src/*": ["./*"] 13 | } 14 | }, 15 | "exclude": ["node_modules", ".vscode-test"] 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coding plugin for VS Code. 2 | 3 | ## How to use 4 | 5 | 1. Install the plugin 6 | 1. Open a repo hosted by `coding.net` 7 | 1. Click on `Log in` button 8 | 1. You will be redirected back to the editor, feel free to try & open an issue 9 | 10 | ## Development 11 | 12 | - Open this example in VS Code 1.47+ 13 | - `yarn install` 14 | - `yarn watch` or `yarn compile` 15 | - `F5` to start debugging 16 | -------------------------------------------------------------------------------- /webviews/components/mr/EditButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import EditIcon from 'webviews/assets/edit.svg'; 4 | import IconButton from 'webviews/components/IconButton'; 5 | 6 | interface Props { 7 | onClick?: () => void; 8 | } 9 | 10 | const EditButton = ({ onClick = () => null }: Props) => { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default EditButton; 19 | -------------------------------------------------------------------------------- /webviews/hooks/persistDataHook.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | import { persistData, removeDataPersist } from 'webviews/store/appStore'; 3 | 4 | const persistDataHook = () => { 5 | const effect = useRef(() => { 6 | }); 7 | 8 | useEffect(() => { 9 | effect.current = persistData(); 10 | 11 | return () => { 12 | removeDataPersist(effect.current); 13 | }; 14 | }, []); 15 | 16 | return effect.current; 17 | } 18 | 19 | export default persistDataHook; 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /webviews/assets/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: ['@typescript-eslint'], 7 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 8 | rules: { 9 | semi: [2, 'always'], 10 | '@typescript-eslint/no-unused-vars': 0, 11 | '@typescript-eslint/no-explicit-any': 0, 12 | '@typescript-eslint/explicit-module-boundary-types': 0, 13 | '@typescript-eslint/no-non-null-assertion': 0, 14 | 'no-case-declarations': 0, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /webviews/store/constants.ts: -------------------------------------------------------------------------------- 1 | export enum actions { 2 | UPDATE_CURRENT_MR = `mr.update`, 3 | CLOSE_MR = `mr.close`, 4 | MR_APPROVE = `mr.approve`, 5 | MR_DISAPPROVE = `mr.disapprove`, 6 | MR_MERGE = `mr.merge`, 7 | MR_UPDATE_TITLE = `mr.update.title`, 8 | MR_UPDATE_COMMENTS = `mr.update.comments`, 9 | MR_ADD_COMMENT = `mr.add.comment`, 10 | MR_GET_ACTIVITIES = `mr.get.activities`, 11 | MR_TOGGLE_LOADING = `mr.update.toggleLoading`, 12 | MR_UPDATE_REVIEWERS = `mr.update.reviewers`, 13 | MR_UPDATE_DESC = `mr.update.desc`, 14 | MR_REVIEWERS_INIT = `mr.reviewers.init`, 15 | MR_ACTIVITIES_INIT = `mr.activities.init`, 16 | MR_FETCH_STATUS = `mr.fetch.status`, 17 | } 18 | -------------------------------------------------------------------------------- /webviews/components/mr/User.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { IMRDetailMR } from 'src/typings/respResult'; 4 | 5 | const Link = styled.a` 6 | position: relative; 7 | top: 2px; 8 | `; 9 | const AvatarImg = styled.img` 10 | width: 24px; 11 | height: 24px; 12 | border-radius: 50%; 13 | `; 14 | 15 | export const Avatar = ({ for: author = {} }: { for: Partial }) => ( 16 | 17 | 18 | 19 | ); 20 | 21 | export const AuthorLink = ({ for: author }: { for: IMRDetailMR['author'] }) => ( 22 | {author?.name} 23 | ); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./", 4 | "baseUrl": "./", 5 | "outDir": "out/webviews", 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": ["es2020", "es6", "dom", "esnext.asynciterable"], 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "jsx": "react", 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "suppressImplicitAnyIndexErrors": true, 19 | "noUnusedLocals": true, 20 | "skipLibCheck": true, 21 | "allowSyntheticDefaultImports": true, 22 | "paths": { 23 | "webviews/*": ["webviews/*"], 24 | "src/*": ["src/*"] 25 | } 26 | }, 27 | "include": ["webviews", "src/typings"] 28 | } 29 | -------------------------------------------------------------------------------- /webviews/hooks/initDataHook.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import appStore from 'webviews/store/appStore'; 3 | import { actions } from 'webviews/store/constants'; 4 | 5 | export default function initDataHook() { 6 | useEffect(() => { 7 | window.addEventListener('message', (ev) => { 8 | const { updateCurrentMR, toggleMRLoading, initMRReviewers, initMRActivities } = appStore; 9 | const { command, res } = ev?.data; 10 | 11 | switch (command) { 12 | case actions.MR_TOGGLE_LOADING: { 13 | toggleMRLoading(); 14 | break; 15 | } 16 | case actions.UPDATE_CURRENT_MR: { 17 | updateCurrentMR(res); 18 | break; 19 | } 20 | case actions.MR_REVIEWERS_INIT: { 21 | initMRReviewers(res); 22 | break; 23 | } 24 | case actions.MR_ACTIVITIES_INIT: { 25 | initMRActivities(res); 26 | break; 27 | } 28 | default: 29 | break; 30 | } 31 | }); 32 | }, []); 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | main: './webviews/index.tsx', 6 | }, 7 | output: { 8 | path: path.resolve(__dirname, 'out/webviews'), 9 | filename: '[name].js', 10 | }, 11 | devtool: 'eval-source-map', 12 | resolve: { 13 | extensions: ['.js', '.ts', '.tsx', '.json'], 14 | alias: { 15 | webviews: path.resolve(__dirname, `webviews`), 16 | src: path.resolve(__dirname, `src`), 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(ts|tsx)$/, 23 | loader: 'ts-loader', 24 | options: {}, 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: [ 29 | { 30 | loader: 'style-loader', 31 | }, 32 | { 33 | loader: 'css-loader', 34 | }, 35 | ], 36 | }, 37 | { 38 | test: /\.svg$/, 39 | use: ['@svgr/webpack'], 40 | }, 41 | ], 42 | }, 43 | performance: { 44 | hints: false, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/typings/commonTypes.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IDiffFile, IMRDetail, IMRStatusItem, IUserItem } from './respResult'; 3 | 4 | export interface IRepoInfo { 5 | team: string; 6 | project: string; 7 | repo: string; 8 | } 9 | 10 | export enum TokenType { 11 | AccessToken = `accessToken`, 12 | RefreshToken = `refreshToken`, 13 | } 14 | 15 | export interface ISessionData { 16 | id: string; 17 | user: IUserItem | null; 18 | accessToken: string; 19 | refreshToken: string; 20 | } 21 | 22 | export enum GitChangeType { 23 | ADD = `ADD`, 24 | COPY = `COPY`, 25 | DELETE = `DELETE`, 26 | MODIFY = `MODIFY`, 27 | RENAME = `RENAME`, 28 | TYPE = `TYPE`, 29 | UNKNOWN = `UNKNOWN`, 30 | UNMERGED = `UNMERGED`, 31 | } 32 | 33 | export interface IMRWebViewDetail { 34 | type: string; 35 | iid: string; 36 | accessToken: string; 37 | repoInfo: IRepoInfo; 38 | data: IMRDetail & { 39 | loading: boolean; 40 | editingDesc: boolean; 41 | commit_statuses: IMRStatusItem[]; 42 | }; 43 | user: IUserItem; 44 | } 45 | 46 | export interface IDiffFileData { 47 | [key: string]: IDiffFile; 48 | } 49 | 50 | export interface ICachedCommentThreads { 51 | [key: string]: vscode.CommentThread[]; 52 | } 53 | 54 | export interface ICachedCommentController { 55 | [key: string]: vscode.CommentController; 56 | } 57 | -------------------------------------------------------------------------------- /webviews/app.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import EditIcon from 'webviews/assets/edit.svg'; 4 | 5 | export const EmptyWrapper = styled.div` 6 | font-size: 16px; 7 | `; 8 | export const TitleWrapper = styled.div` 9 | display: flex; 10 | align-items: center; 11 | font-size: 20px; 12 | height: 38px; 13 | 14 | .edit { 15 | display: none; 16 | } 17 | 18 | &:hover .edit { 19 | display: block; 20 | } 21 | `; 22 | export const Row = styled.div` 23 | display: flex; 24 | align-items: center; 25 | margin: 16px 0 0; 26 | padding-bottom: 15px; 27 | border-bottom: 1px solid var(--vscode-list-inactiveSelectionBackground); 28 | `; 29 | export const Desc = styled.article` 30 | border: 1px solid var(--vscode-list-inactiveSelectionBackground); 31 | padding: 10px; 32 | `; 33 | export const BodyWrap = styled.div` 34 | display: flex; 35 | `; 36 | export const Body = styled.div` 37 | flex: 1; 38 | `; 39 | export const Sidebar = styled.div` 40 | width: 200px; 41 | margin-left: 20px; 42 | `; 43 | export const EditBtn = styled(EditIcon)` 44 | width: 16px; 45 | height: 16px; 46 | margin-left: 10px; 47 | cursor: pointer; 48 | `; 49 | export const Empty = styled.div` 50 | text-align: center; 51 | `; 52 | 53 | export const BranchName = styled.code` 54 | margin: 0 1ex; 55 | `; 56 | 57 | export const OperationBtn = styled.button` 58 | margin-left: 1em; 59 | `; 60 | 61 | export const SectionTitle = styled.h3` 62 | line-height: 28px; 63 | `; 64 | -------------------------------------------------------------------------------- /src/common/logger.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | type LogLevel = 'Trace' | 'Info' | 'Error'; 4 | 5 | class Log { 6 | private output: vscode.OutputChannel; 7 | 8 | constructor() { 9 | this.output = vscode.window.createOutputChannel('GitHub Authentication'); 10 | } 11 | 12 | private data2String(data: any): string { 13 | if (data instanceof Error) { 14 | return data.stack || data.message; 15 | } 16 | if (data.success === false && data.message) { 17 | return data.message; 18 | } 19 | return data.toString(); 20 | } 21 | 22 | public info(message: string, data?: any): void { 23 | this.logLevel('Info', message, data); 24 | } 25 | 26 | public error(message: string, data?: any): void { 27 | this.logLevel('Error', message, data); 28 | } 29 | 30 | public logLevel(level: LogLevel, message: string, data?: any): void { 31 | this.output.appendLine(`[${level} - ${this.now()}] ${message}`); 32 | if (data) { 33 | this.output.appendLine(this.data2String(data)); 34 | } 35 | } 36 | 37 | private now(): string { 38 | const now = new Date(); 39 | return ( 40 | padLeft(now.getUTCHours() + '', 2, '0') + 41 | ':' + 42 | padLeft(now.getMinutes() + '', 2, '0') + 43 | ':' + 44 | padLeft(now.getUTCSeconds() + '', 2, '0') + 45 | '.' + 46 | now.getMilliseconds() 47 | ); 48 | } 49 | } 50 | 51 | function padLeft(s: string, n: number, pad = ' ') { 52 | return pad.repeat(Math.max(0, n - s.length)) + s; 53 | } 54 | 55 | const Logger = new Log(); 56 | export default Logger; 57 | -------------------------------------------------------------------------------- /src/common/gitService.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as cp from 'child_process'; 3 | 4 | import { Git, GitExtension } from 'src/typings/git'; 5 | 6 | export class GitService { 7 | static git: Git; 8 | 9 | static async init() { 10 | const extension = vscode.extensions.getExtension( 11 | `vscode.git`, 12 | ) as vscode.Extension; 13 | if (extension !== undefined) { 14 | const gitExtension = extension.isActive ? extension.exports : await extension.activate(); 15 | const model = gitExtension.getAPI(1); 16 | GitService.git = model.git; 17 | } 18 | } 19 | 20 | static async getRemoteURLs(): Promise { 21 | try { 22 | if (!GitService.git || !vscode.workspace.workspaceFolders?.length) { 23 | return null; 24 | } 25 | 26 | const tasks = vscode.workspace.workspaceFolders.map( 27 | (f) => 28 | new Promise((resolve) => { 29 | cp.exec( 30 | `${GitService.git.path} -C "${f.uri.path}" config --get remote.origin.url`, 31 | { 32 | cwd: f.uri.path, 33 | }, 34 | (err, stdout, stderr) => { 35 | resolve({ stdout, stderr }); 36 | }, 37 | ); 38 | }), 39 | ); 40 | 41 | const result = await Promise.all(tasks); 42 | const urls = result.map((o) => { 43 | return (o as { stdout: string; stderr: string }).stdout?.trim(); 44 | }); 45 | 46 | return urls; 47 | } catch (err) { 48 | console.error(err); 49 | } 50 | 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /webviews/components/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { css, keyframes } from 'styled-components'; 3 | 4 | interface Props { 5 | onClick?: () => void; 6 | children: React.ReactElement; 7 | title?: string; 8 | width?: number; 9 | height?: number; 10 | rotate?: boolean; 11 | } 12 | 13 | const rotate = keyframes` 14 | 0% { 15 | transform: rotate(0deg); 16 | } 17 | 18 | 100% { 19 | transform: rotate(360deg); 20 | } 21 | `; 22 | 23 | const Button = styled(({ height, width, rotate, ...rest }: Props) => 70 | ); 71 | }; 72 | 73 | export default IconButton; 74 | -------------------------------------------------------------------------------- /src/tree/releaseTree.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | 4 | enum ItemType { 5 | ListItem = `listItem`, 6 | FolderITem = `folderItem`, 7 | CategoryItem = `categoryItem`, 8 | } 9 | 10 | export class ReleaseTreeDataProvider implements vscode.TreeDataProvider { 11 | private _onDidChangeTreeData: vscode.EventEmitter< 12 | ListItem | undefined | void 13 | > = new vscode.EventEmitter(); 14 | readonly onDidChangeTreeData: vscode.Event = this 15 | ._onDidChangeTreeData.event; 16 | 17 | private _context: vscode.ExtensionContext; 18 | 19 | constructor(context: vscode.ExtensionContext) { 20 | this._context = context; 21 | } 22 | 23 | public refresh(): any { 24 | this._onDidChangeTreeData.fire(); 25 | } 26 | 27 | getTreeItem(element: ListItem): vscode.TreeItem { 28 | return element; 29 | } 30 | 31 | getChildren(element?: ListItem): Thenable { 32 | if (element) { 33 | return Promise.resolve([]); 34 | } 35 | 36 | return Promise.resolve([ 37 | new ListItem(`//TODO`, `noData`, vscode.TreeItemCollapsibleState.None), 38 | ]); 39 | } 40 | } 41 | 42 | export class ListItem extends vscode.TreeItem { 43 | contextValue = ItemType.ListItem; 44 | private readonly _value: string | number; 45 | 46 | constructor( 47 | public readonly label: string, 48 | public readonly val: number | string, 49 | public readonly collapsibleState: vscode.TreeItemCollapsibleState, 50 | public readonly command?: vscode.Command, 51 | ) { 52 | super(label, collapsibleState); 53 | 54 | this._value = val; 55 | } 56 | 57 | get value() { 58 | return this._value; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /webviews/components/mr/Activities.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { view } from '@risingstack/react-easy-state'; 3 | import appStore from 'webviews/store/appStore'; 4 | 5 | import Activity from './Activity'; 6 | import Comment from './Comment'; 7 | import AddComment from './AddComment'; 8 | import { IActivity, IComment } from 'src/typings/respResult'; 9 | 10 | function Activities() { 11 | const { currentMR, activities, comments } = appStore; 12 | 13 | const renderActivity = (activity: [IActivity]) => { 14 | return ( 15 |
16 | 21 |
22 | ); 23 | }; 24 | 25 | const renderComment = (comment: [IComment]) => { 26 | return ( 27 |
28 | 29 |
30 | ); 31 | }; 32 | 33 | const allActivities = [...activities.map((i) => [i]), ...comments].sort( 34 | (a, b) => a[0]?.created_at - b[0]?.created_at, 35 | ); 36 | 37 | if (!allActivities.length) { 38 | return
Loading...
; 39 | } 40 | 41 | return ( 42 |
43 |
44 | {allActivities.map((activity: any) => { 45 | if (activity[0]?.action) { 46 | return renderActivity(activity as [IActivity]); 47 | } else if (!activity[0]?.action) { 48 | return renderComment(activity as [IComment]); 49 | } 50 | return null; 51 | })} 52 |
53 |
54 | 55 |
56 |
57 | ); 58 | } 59 | 60 | export default view(Activities); 61 | -------------------------------------------------------------------------------- /webviews/assets/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /webviews/components/mr/Reviewers.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import styled from 'styled-components'; 3 | import { view } from '@risingstack/react-easy-state'; 4 | 5 | import appStore from 'webviews/store/appStore'; 6 | import { Avatar, AuthorLink } from 'webviews/components/mr/User'; 7 | import EditButton from 'webviews/components/mr/EditButton'; 8 | 9 | const Title = styled.div` 10 | margin-top: 15px; 11 | font-size: 16px; 12 | font-weight: 600; 13 | `; 14 | const FlexCenter = styled.div` 15 | display: flex; 16 | align-items: center; 17 | `; 18 | const Reviewer = styled(FlexCenter)` 19 | padding: 5px 0; 20 | justify-content: space-between; 21 | 22 | a:first-child { 23 | margin-right: 5px; 24 | } 25 | `; 26 | 27 | function Reviewers() { 28 | const { reviewers, currentMR } = appStore; 29 | const { reviewers: rReviewers = [], volunteer_reviewers: volunteerReviewers = [] } = reviewers; 30 | const allReviewers = [...rReviewers, ...volunteerReviewers]; 31 | const { updateReviewers } = appStore; 32 | 33 | const onUpdateReviewer = useCallback(() => { 34 | const list = allReviewers.map((i) => i.reviewer.id); 35 | updateReviewers(currentMR.iid, list, currentMR.data.merge_request.author.global_key); 36 | }, [allReviewers]); 37 | 38 | return ( 39 |
40 | 41 | Reviewers 42 | <EditButton onClick={onUpdateReviewer} /> 43 | 44 | {allReviewers.map((r) => { 45 | return ( 46 | 47 | 48 | 49 | 50 | 51 | {r.value === 100 && `👍`} 52 | 53 | ); 54 | })} 55 |
56 | ); 57 | } 58 | 59 | export default view(Reviewers); 60 | -------------------------------------------------------------------------------- /webviews/components/mr/StatusCheck.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { sleep } from 'webviews/utils/helper'; 5 | import { IMRStatus } from 'src/typings/respResult'; 6 | import { SectionTitle } from 'webviews/app.styles'; 7 | import RefreshIcon from 'webviews/assets/refresh.svg'; 8 | import IconButton from 'webviews/components/IconButton'; 9 | 10 | interface Props { 11 | data: IMRStatus | null; 12 | onRefresh: (...args: any[]) => Promise; 13 | } 14 | 15 | const ListItem = styled.li` 16 | i { 17 | margin-left: 2ex; 18 | } 19 | `; 20 | 21 | function StatusCheck(props: Props) { 22 | const { data } = props; 23 | const [refreshing, setRefreshing] = useState(false); 24 | 25 | const onRefresh = async () => { 26 | if (refreshing) { 27 | return; 28 | } 29 | 30 | setRefreshing(true); 31 | // minimum 1s 32 | await Promise.allSettled([props.onRefresh, sleep(1000)]); 33 | setRefreshing(false); 34 | }; 35 | 36 | return ( 37 | <> 38 | 39 | Status Check 40 | 46 | 47 | 48 | 49 |
    50 | {data?.statuses ? ( 51 | data?.statuses.map((i) => { 52 | return ( 53 | 54 | {i.context} 55 | 56 | {i.description} 57 | 58 | 59 | ); 60 | }) 61 | ) : ( 62 |
  • No related job found
  • 63 | )} 64 |
65 | 66 | ); 67 | } 68 | 69 | export default StatusCheck; 70 | -------------------------------------------------------------------------------- /src/tree/inMemMRContentProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as vscode from 'vscode'; 4 | import { CodingServer } from 'src/codingServer'; 5 | 6 | export class InMemMRContentProvider implements vscode.TextDocumentContentProvider { 7 | private _onDidChange = new vscode.EventEmitter(); 8 | private _context: vscode.ExtensionContext; 9 | private _service: CodingServer; 10 | 11 | get onDidChange(): vscode.Event { 12 | return this._onDidChange.event; 13 | } 14 | 15 | fireDidChange(uri: vscode.Uri) { 16 | this._onDidChange.fire(uri); 17 | } 18 | 19 | private _mrFileChangeContentProviders: { 20 | [key: number]: (uri: vscode.Uri) => Promise; 21 | } = {}; 22 | 23 | constructor(context: vscode.ExtensionContext, service: CodingServer) { 24 | this._context = context; 25 | this._service = service; 26 | } 27 | 28 | async provideTextDocumentContent( 29 | uri: vscode.Uri, 30 | token: vscode.CancellationToken, 31 | ): Promise { 32 | const params = new URLSearchParams(decodeURIComponent(uri.query)); 33 | const commit = params.get(`right`) === `true` ? params.get(`rightSha`) : params.get('leftSha'); 34 | const path = params.get(`path`); 35 | return await this._service.getRemoteFileContent(`${commit}/${path}`); 36 | } 37 | 38 | registerTextDocumentContentProvider( 39 | mrNumber: number, 40 | provider: (uri: vscode.Uri) => Promise, 41 | ): vscode.Disposable { 42 | this._mrFileChangeContentProviders[mrNumber] = provider; 43 | 44 | return { 45 | dispose: () => { 46 | delete this._mrFileChangeContentProviders[mrNumber]; 47 | }, 48 | }; 49 | } 50 | } 51 | 52 | export function getInMemMRContentProvider( 53 | context: vscode.ExtensionContext, 54 | service: CodingServer, 55 | ): InMemMRContentProvider { 56 | return new InMemMRContentProvider(context, service); 57 | } 58 | -------------------------------------------------------------------------------- /webviews/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 3 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | margin: 0; 7 | } 8 | 9 | code { 10 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 11 | } 12 | 13 | #root { 14 | padding: 1em; 15 | } 16 | 17 | textarea, 18 | input[type='text'] { 19 | display: block; 20 | box-sizing: border-box; 21 | padding: 10px; 22 | width: 100%; 23 | resize: vertical; 24 | font-size: 13px; 25 | border: 1px solid var(--vscode-dropdown-border); 26 | background-color: var(--vscode-input-background); 27 | color: var(--vscode-input-foreground); 28 | font-family: var(--vscode-editor-font-family); 29 | } 30 | 31 | textarea { 32 | min-height: 100px; 33 | max-height: 500px; 34 | } 35 | 36 | textarea:focus { 37 | outline: 1px solid var(--vscode-focusBorder); 38 | } 39 | 40 | button.colored { 41 | background-color: var(--vscode-button-background); 42 | color: var(--vscode-button-foreground); 43 | border-radius: 0px; 44 | border: 1px solid transparent; 45 | outline: none; 46 | padding: 4px 12px; 47 | font-size: 13px; 48 | line-height: 18px; 49 | white-space: nowrap; 50 | user-select: none; 51 | cursor: pointer; 52 | opacity: 0.9; 53 | } 54 | 55 | button.colored:hover { 56 | opacity: 1; 57 | } 58 | 59 | button.colored.secondary { 60 | background-color: var(--vscode-button-secondaryBackground); 61 | color: var(--vscode-button-secondaryForeground); 62 | } 63 | 64 | button.colored:disabled { 65 | cursor: not-allowed; 66 | opacity: 0.5; 67 | } 68 | 69 | #status { 70 | box-sizing: border-box; 71 | line-height: 18px; 72 | background: var(--vscode-badge-background); 73 | color: var(--vscode-badge-foreground); 74 | border-radius: 4px; 75 | padding: 2px 8px; 76 | margin-right: 12px; 77 | } 78 | -------------------------------------------------------------------------------- /webviews/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { vscode } from 'webviews/constants/vscode'; 2 | import { nanoid } from 'nanoid'; 3 | import { IRequestMessage, IReplyMessage } from 'src/typings/message'; 4 | 5 | export class MessageHandler { 6 | private _commandHandler: ((message: any) => void) | null; 7 | // private lastSentReq: string; 8 | private pendingReplies: any; 9 | 10 | constructor(commandHandler: any) { 11 | this._commandHandler = commandHandler; 12 | // this.lastSentReq = nanoid(); 13 | this.pendingReplies = Object.create(null); 14 | window.addEventListener('message', this.handleMessage.bind(this)); 15 | } 16 | 17 | public registerCommandHandler(commandHandler: (message: any) => void) { 18 | this._commandHandler = commandHandler; 19 | } 20 | 21 | public async postMessage(message: any): Promise { 22 | const req = nanoid(); 23 | return new Promise((resolve, reject) => { 24 | this.pendingReplies[req] = { 25 | resolve: resolve, 26 | reject: reject, 27 | }; 28 | message = Object.assign(message, { 29 | req: req, 30 | }); 31 | vscode.postMessage(message as IRequestMessage); 32 | }); 33 | } 34 | 35 | // handle message should resolve promises 36 | private handleMessage(event: any) { 37 | const message: IReplyMessage = event.data; 38 | if (message.seq) { 39 | const pendingReply = this.pendingReplies[message.seq]; 40 | if (pendingReply) { 41 | if (message.err) { 42 | pendingReply.reject(message.err); 43 | } else { 44 | pendingReply.resolve(message.res); 45 | } 46 | return; 47 | } 48 | } 49 | 50 | if (this._commandHandler) { 51 | this._commandHandler(message.res); 52 | } 53 | } 54 | } 55 | 56 | export function getMessageHandler(handler: ((message: any) => void) | null) { 57 | let instance: MessageHandler; 58 | 59 | return () => { 60 | if (!instance) { 61 | instance = new MessageHandler(handler); 62 | } 63 | 64 | return instance; 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /webviews/components/mr/Comment.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { IComment } from 'src/typings/respResult'; 4 | import { Avatar, AuthorLink } from './User'; 5 | import { getTime } from 'webviews/utils/time'; 6 | 7 | interface IProps { 8 | comment: IComment; 9 | } 10 | 11 | const Root = styled.p``; 12 | 13 | const Header = styled.div` 14 | display: flex; 15 | align-items: center; 16 | border: 1px solid var(--vscode-list-inactiveSelectionBackground); 17 | background: var(--vscode-list-inactiveSelectionBackground); 18 | padding: 5px 10px; 19 | `; 20 | const AuthorLinkWrap = styled.div` 21 | margin-left: 5px; 22 | `; 23 | const Body = styled.div` 24 | padding: 10px; 25 | border: 1px solid var(--vscode-list-inactiveSelectionBackground); 26 | border-top: none; 27 | `; 28 | const Time = styled.div` 29 | margin-left: 15px; 30 | `; 31 | 32 | const ChildComment = styled.div` 33 | display: flex; 34 | align-items: center; 35 | border-top: 1px solid var(--vscode-list-inactiveSelectionBackground); 36 | `; 37 | const ChildCommentContent = styled.div` 38 | margin-left: 10px; 39 | `; 40 | 41 | function Comment({ comment }: IProps) { 42 | const renderChildComments = () => { 43 | return comment?.childComments?.map((c) => { 44 | return ( 45 | 46 | 47 | 48 |
49 | 50 | 51 | ); 52 | }); 53 | }; 54 | 55 | return ( 56 | 57 |
58 | {' '} 59 | 60 | 61 | 62 | 63 |
64 | 65 |
66 | {renderChildComments()} 67 | 68 | 69 | ); 70 | } 71 | 72 | export default Comment; 73 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { Event, Disposable, Uri } from 'vscode'; 2 | import { IRepoInfo } from 'src/typings/commonTypes'; 3 | 4 | export interface PromiseAdapter { 5 | (value: T, resolve: (value: U | PromiseLike) => void, reject: (reason: any) => void): any; 6 | } 7 | 8 | const passthrough = (value: any, resolve: (value?: any) => void) => resolve(value); 9 | 10 | export async function promiseFromEvent( 11 | event: Event, 12 | adapter: PromiseAdapter = passthrough, 13 | ): Promise { 14 | let subscription: Disposable; 15 | return new Promise( 16 | (resolve, reject) => 17 | (subscription = event((value: T) => { 18 | try { 19 | Promise.resolve(adapter(value, resolve, reject)).catch(reject); 20 | } catch (error) { 21 | reject(error); 22 | } 23 | })), 24 | ).then( 25 | (result: U) => { 26 | subscription.dispose(); 27 | return result; 28 | }, 29 | (error) => { 30 | subscription.dispose(); 31 | throw error; 32 | }, 33 | ); 34 | } 35 | 36 | export function parseQuery(uri: Uri) { 37 | return uri.query.split('&').reduce((prev: any, current) => { 38 | const queryString = current.split('='); 39 | prev[queryString[0]] = queryString[1]; 40 | return prev; 41 | }, {}); 42 | } 43 | 44 | export function parseCloneUrl(url: string): IRepoInfo | null { 45 | const reg = /^(https:\/\/|git@)e\.coding\.net(\/|:)(.*)\.git$/i; 46 | const result = url.match(reg); 47 | 48 | if (!result) { 49 | return null; 50 | } 51 | 52 | const str = result.pop(); 53 | if (!str || !str?.includes(`/`)) { 54 | return null; 55 | } 56 | 57 | const [team, project, repo] = str.split(`/`); 58 | return { team, project, repo: repo || project }; 59 | } 60 | 61 | export function getNonce() { 62 | let text = ''; 63 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 64 | for (let i = 0; i < 32; i++) { 65 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 66 | } 67 | return text; 68 | } 69 | 70 | const HunkRegExp = /@@.+@@/g; 71 | export const isHunkLine = (hunk: string) => HunkRegExp.test(hunk); 72 | 73 | export const getDiffLineNumber = (hunk: string) => { 74 | const matchedHunks = hunk.match(/[-+]\d+(,\d+)?/g) || []; 75 | return matchedHunks.map((i) => { 76 | const [start = 0, sum = 0] = i.match(/\d+/g)?.map((j) => +j) ?? []; 77 | const end = start + sum > 0 ? start + sum - 1 : 0; 78 | return [start, end]; 79 | }); 80 | }; 81 | -------------------------------------------------------------------------------- /src/common/keychain.ts: -------------------------------------------------------------------------------- 1 | // keytar depends on a native module shipped in vscode, so this is 2 | // how we load it 3 | import * as vscode from 'vscode'; 4 | import * as keytar from 'keytar'; 5 | import type * as keytarType from 'keytar'; 6 | // import * as nls from 'vscode-nls'; 7 | 8 | import Logger from './logger'; 9 | import { TokenType } from '../typings/commonTypes'; 10 | 11 | // const localize = nls.loadMessageBundle(); 12 | 13 | function getKeytar(): Keytar | undefined { 14 | try { 15 | return require('keytar'); 16 | } catch (err) { 17 | console.log(err); 18 | } 19 | 20 | return undefined; 21 | } 22 | 23 | export type Keytar = { 24 | getPassword: typeof keytarType['getPassword']; 25 | setPassword: typeof keytarType['setPassword']; 26 | deletePassword: typeof keytarType['deletePassword']; 27 | }; 28 | 29 | const SERVICE_ID = `coding.auth`; 30 | 31 | export class Keychain { 32 | async setToken(token: string, t: TokenType): Promise { 33 | try { 34 | return await keytar.setPassword(SERVICE_ID, t, token); 35 | } catch (e) { 36 | // Ignore 37 | Logger.error(`Setting token failed: ${e}`); 38 | await vscode.window.showErrorMessage( 39 | `Writing login information to the keychain failed with error: ${e.message}.`, 40 | ); 41 | } 42 | } 43 | 44 | async getToken(t: TokenType): Promise { 45 | try { 46 | return await keytar.getPassword(SERVICE_ID, t); 47 | } catch (e) { 48 | // Ignore 49 | Logger.error(`Getting token failed: ${e}`); 50 | return Promise.resolve(undefined); 51 | } 52 | } 53 | 54 | async tryMigrate(t: TokenType): Promise { 55 | try { 56 | const keytar = getKeytar(); 57 | if (!keytar) { 58 | throw new Error('keytar unavailable'); 59 | } 60 | 61 | const oldValue = await keytar.getPassword(`${vscode.env.uriScheme}-coding.login`, 'account'); 62 | if (oldValue) { 63 | await this.setToken(oldValue, t); 64 | await keytar.deletePassword(`${vscode.env.uriScheme}-coding.login`, 'account'); 65 | } 66 | 67 | return oldValue; 68 | } catch (_) { 69 | // Ignore 70 | return Promise.resolve(undefined); 71 | } 72 | } 73 | 74 | async deleteToken(t: TokenType): Promise { 75 | try { 76 | return await keytar.deletePassword(SERVICE_ID, t); 77 | } catch (e) { 78 | // Ignore 79 | Logger.error(`Deleting token failed: ${e}`); 80 | return Promise.resolve(undefined); 81 | } 82 | } 83 | } 84 | 85 | export const keychain = new Keychain(); 86 | -------------------------------------------------------------------------------- /webviews/components/mr/Activity.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import { view } from '@risingstack/react-easy-state'; 4 | import appStore from 'webviews/store/appStore'; 5 | import { getTime } from 'webviews/utils/time'; 6 | import { ACTIVITY_TYPE } from 'webviews/constants/activity'; 7 | import { IActivity } from 'src/typings/respResult'; 8 | 9 | interface IProps { 10 | activity: IActivity; 11 | srcBranch: string; 12 | desBranch: string; 13 | } 14 | 15 | const Time = styled.span` 16 | margin-left: 20px; 17 | `; 18 | const ActionDesc = styled.span` 19 | margin-left: 1ex; 20 | margin-right: 1ex; 21 | `; 22 | 23 | function Activity({ activity, srcBranch, desBranch }: IProps) { 24 | const { currentMR } = appStore; 25 | const { repoInfo } = currentMR; 26 | 27 | const { action } = activity; 28 | if (!ACTIVITY_TYPE[action]) { 29 | return null; 30 | } 31 | 32 | const pathForTargetResource = (action: string, path: string) => { 33 | if ( 34 | action !== 'add_extlink_ref' && 35 | action !== 'add_external_extlink_ref' && 36 | action !== 'del_extlink_ref' && 37 | action !== 'del_external_extlink_ref' 38 | ) { 39 | return path; 40 | } 41 | 42 | const regex = new RegExp('^(http|https|mailto|ftp)', 'i'); 43 | if (regex.test(path)) { 44 | return path; 45 | } 46 | return `http://${path}`; 47 | }; 48 | 49 | const renderDesc = (info: any, action: string) => { 50 | if (info.reviewer) return info.reviewer.name; 51 | if (info.watcher) return info.watcher.name; 52 | if (info.label) { 53 | const labelObj = JSON.parse(info.label); 54 | return labelObj.name; 55 | } 56 | 57 | if (info.internal_resource) { 58 | const url = pathForTargetResource(action, info.internal_resource.path); 59 | return {info.internal_resource.name}; 60 | } 61 | 62 | if (info.external_resource) { 63 | return ( 64 | 65 | 66 | {info.external_resource.project_display_name || info.external_resource.project_name} 67 | {' '} 68 | {info.external_resource.conj}{' '} 69 | 70 | {info.external_resource.name} 71 | 72 | 73 | ); 74 | } 75 | }; 76 | 77 | const authorUrl = `https://${repoInfo.team}.coding.net${activity.author.path}`; 78 | 79 | return ( 80 |
81 | {ACTIVITY_TYPE[action].icon} 82 |
83 |

84 | {activity.author.name}{' '} 85 | {ACTIVITY_TYPE[action].text} 86 | {(action === 'del_source_branch' || action === 'restore_source_branch') && ( 87 | {srcBranch} 88 | )} 89 | {(action === 'del_target_branch' || action === 'restore_target_branch') && ( 90 | {desBranch} 91 | )} 92 | 93 | {renderDesc(activity.comment || {}, action)} 94 | 95 | 96 |

97 |
98 |
99 | ); 100 | } 101 | 102 | export default view(Activity); 103 | -------------------------------------------------------------------------------- /webviews/components/mr/AddComment.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { view } from '@risingstack/react-easy-state'; 3 | import appStore from 'webviews/store/appStore'; 4 | import styled from 'styled-components'; 5 | 6 | import { MERGE_STATUS } from 'webviews/constants/mergeRequest'; 7 | 8 | const ActionWrap = styled.div` 9 | margin-top: 10px; 10 | text-align: right; 11 | button { 12 | margin-right: 10px; 13 | } 14 | button:last-child { 15 | margin-right: 0; 16 | } 17 | `; 18 | 19 | function AddComment() { 20 | const { currentMR, reviewers, closeMR, approveMR, disapproveMR, mergeMR, commentMR } = appStore; 21 | const { 22 | data: { merge_request: mergeRequest, can_merge: canMerge }, 23 | user, 24 | } = currentMR; 25 | 26 | const [isBusy, setBusy] = useState(false); 27 | const [comment, setComment] = useState(); 28 | 29 | const mrStatus = mergeRequest?.merge_status; 30 | const showCloseBtn = 31 | (canMerge || user?.id === mergeRequest?.author?.id) && 32 | mrStatus !== MERGE_STATUS.REFUSED && 33 | mrStatus !== MERGE_STATUS.ACCEPTED; 34 | const mrStatusOk = mrStatus === MERGE_STATUS.CANMERGE || mrStatus === MERGE_STATUS.CANNOTMERGE; 35 | const showMergeBtn = mrStatus === MERGE_STATUS.CANMERGE; 36 | const showAllowMergeBtn = mrStatusOk && mergeRequest?.author?.id !== user?.id; 37 | 38 | const getAgreed = () => { 39 | const index = reviewers.reviewers.findIndex((r) => r.reviewer.id === user.id); 40 | let agreed = reviewers.volunteer_reviewers.findIndex((r) => r.reviewer.id === user.id) >= 0; 41 | if (index >= 0) { 42 | agreed = reviewers.reviewers[index].value === 100; 43 | } 44 | 45 | return agreed; 46 | }; 47 | 48 | const submit = async (command: (body?: string) => Promise) => { 49 | try { 50 | setBusy(true); 51 | await command(comment); 52 | setComment(''); 53 | } finally { 54 | setBusy(false); 55 | } 56 | }; 57 | 58 | const handleClick = (event: any) => { 59 | const { command } = event.target.dataset; 60 | submit({ closeMR, approveMR, disapproveMR, mergeMR, commentMR }[command]); 61 | }; 62 | 63 | const handleCommentChange = (event: React.ChangeEvent) => { 64 | setComment(event.target.value); 65 | }; 66 | 67 | const renderActions = () => { 68 | return ( 69 | 70 | {showAllowMergeBtn && !getAgreed() && ( 71 | 78 | )} 79 | {showAllowMergeBtn && getAgreed() && ( 80 | 87 | )} 88 | {showMergeBtn && ( 89 | 96 | )} 97 | {showCloseBtn && ( 98 | 105 | )} 106 | 113 | 114 | ); 115 | }; 116 | 117 | return ( 118 |
119 |