├── .eslintignore ├── src ├── typings │ ├── global.d.ts │ ├── hbuilderx.d.ts │ └── common.ts ├── utils │ ├── email.ts │ ├── password.ts │ ├── proxy.ts │ ├── ip.ts │ ├── toast.ts │ ├── repo.ts │ ├── mr.ts │ ├── axios.ts │ └── actions.ts ├── tsconfig.json ├── init │ ├── createWebview.ts │ ├── initLoginLogout.ts │ ├── initWorkspace.ts │ ├── registerCustomEditors.ts │ ├── createTreeViews.ts │ ├── initCredentials.ts │ └── registerCommands.ts ├── initialize.ts ├── extension.ts ├── trees │ ├── depot.ts │ ├── mr.ts │ └── depotMR.ts ├── webviews │ ├── index.ts │ └── depot.ts ├── services │ ├── git.ts │ ├── dcloud.ts │ └── codingServer.ts └── customEditors │ └── mergeRequest.ts ├── .gitignore ├── webviews ├── utils │ ├── depot.ts │ ├── command.ts │ └── axios.ts ├── components │ ├── DepotSelect │ │ ├── style.css │ │ └── index.tsx │ ├── MergeRequestList │ │ ├── style.css │ │ └── index.tsx │ ├── Actions │ │ └── index.tsx │ └── MergeRequestDetail │ │ ├── style.css │ │ └── index.tsx ├── typings │ ├── global.d.ts │ └── common.ts ├── style.css ├── reducers │ ├── context.ts │ └── index.ts ├── constants │ └── index.ts ├── hooks │ ├── useMountedState.ts │ └── useAsyncFn.ts ├── index.tsx ├── package.json ├── styles │ └── common.css ├── App.tsx ├── services │ └── index.ts └── yarn.lock ├── .prettierrc.js ├── CHANGELOG.md ├── scripts └── release.js ├── .eslintrc.js ├── tsconfig.json ├── docs └── USAGE.md ├── webpack.config.js ├── README.md ├── LICENSE └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | out/* 2 | scripts/* 3 | *.js 4 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface IObject { 2 | [key: string]: any 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .build 3 | build 4 | node_modules/ 5 | build/ 6 | out/ 7 | temp 8 | -------------------------------------------------------------------------------- /src/utils/email.ts: -------------------------------------------------------------------------------- 1 | export const getEmailPrefix = (email: string) => { 2 | const matchRes = email.match(/([^@]+)@/); 3 | return matchRes?.[1]; 4 | }; 5 | -------------------------------------------------------------------------------- /webviews/utils/depot.ts: -------------------------------------------------------------------------------- 1 | export const getDepotProject = (depotPath: string) => { 2 | const matchRes = depotPath.match(/\/p\/([^/]+)/); 3 | return matchRes?.[1]; 4 | }; 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | jsxSingleQuote: true, 6 | printWidth: 120, 7 | tabWidth: 2 8 | } 9 | -------------------------------------------------------------------------------- /webviews/components/DepotSelect/style.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: flex; 3 | align-items: center; 4 | } 5 | 6 | .select { 7 | width: 250px; 8 | color: black; 9 | } 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGE LOG 2 | 3 | ## v1.0.0 (2020-12-01) 4 | 5 | - 新增 CODING 授权; 6 | - 新增 代码仓库列表; 7 | - 新增 创建仓库; 8 | - 新增 合并请求列表; 9 | - 新增 合并请求概览以及合并/允许合并/撤销允许合并/关闭合并请求等操作; 10 | - 新增 将本地代码托管到 CODING; 11 | -------------------------------------------------------------------------------- /src/utils/password.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | 3 | export const encryptPassword = (pwd: string, encryptMethod = 'sha1') => { 4 | const method = crypto.createHash(encryptMethod); 5 | const result = method.update(pwd).digest('hex'); 6 | return result; 7 | }; 8 | -------------------------------------------------------------------------------- /webviews/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | hbuilderx: any; 3 | __CODING__: string; 4 | } 5 | 6 | declare module '*.css' { 7 | const content: any; 8 | export default content; 9 | } 10 | 11 | interface IMsgModel { 12 | command: string; 13 | [key: string]: any; 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es6", "dom"], 6 | "outDir": "../out", 7 | "sourceMap": false, 8 | "strict": true, 9 | "rootDir": ".", 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /webviews/style.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 20px; 3 | font-size: 14px; 4 | } 5 | 6 | .header { 7 | display: flex; 8 | align-items: center; 9 | } 10 | 11 | .actions { 12 | margin-left: 15px; 13 | } 14 | 15 | .mrWrap { 16 | display: flex; 17 | align-items: top; 18 | } 19 | 20 | .left { 21 | margin-right: 30px; 22 | } 23 | -------------------------------------------------------------------------------- /webviews/reducers/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch } from 'react'; 2 | import { initialState, IState, IAction } from './index'; 3 | 4 | interface IDataContext { 5 | state: IState; 6 | dispatch: Dispatch; 7 | } 8 | 9 | export const DataContext = createContext({ 10 | state: initialState, 11 | dispatch: () => {} 12 | }); 13 | -------------------------------------------------------------------------------- /webviews/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const enum MERGE_STATUS { 2 | ACCEPTED = 'ACCEPTED', 3 | REFUSED = 'REFUSED', 4 | CANMERGE = 'CANMERGE', 5 | CANNOTMERGE = 'CANNOTMERGE' 6 | } 7 | 8 | export const MERGE_STATUS_TEXT = { 9 | [MERGE_STATUS.ACCEPTED]: '已合并', 10 | [MERGE_STATUS.CANMERGE]: '可合并', 11 | [MERGE_STATUS.REFUSED]: '已关闭', 12 | [MERGE_STATUS.CANNOTMERGE]: '不可自动合并' 13 | }; 14 | -------------------------------------------------------------------------------- /src/init/createWebview.ts: -------------------------------------------------------------------------------- 1 | import WebviewProvider from '../webviews/depot'; 2 | import ACTIONS, { dispatch } from '../utils/actions'; 3 | import toast from '../utils/toast'; 4 | 5 | function createWebview(context: IContext) { 6 | const webviewProvider = new WebviewProvider(context); 7 | dispatch(ACTIONS.SET_WEBVIEW, { 8 | context, 9 | value: webviewProvider, 10 | }); 11 | } 12 | 13 | export default createWebview; 14 | -------------------------------------------------------------------------------- /webviews/hooks/useMountedState.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | 3 | export default function useMountedState(): () => boolean { 4 | const mountedRef = useRef(false); 5 | const get = useCallback(() => mountedRef.current, []); 6 | 7 | useEffect(() => { 8 | mountedRef.current = true; 9 | 10 | return () => { 11 | mountedRef.current = false; 12 | }; 13 | }); 14 | 15 | return get; 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/proxy.ts: -------------------------------------------------------------------------------- 1 | export const proxy = (target: IObject, source: string, key: string) => { 2 | Object.defineProperty(target, key, { 3 | get() { 4 | return target[source][key]; 5 | }, 6 | set(newValue) { 7 | target[source][key] = newValue; 8 | } 9 | }); 10 | }; 11 | 12 | export const proxyCtx = (target: IObject) => { 13 | const keys = Object.keys(target.ctx); 14 | keys.forEach(k => proxy(target, 'ctx', k)); 15 | }; 16 | -------------------------------------------------------------------------------- /src/init/initLoginLogout.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import { refreshTree } from './registerCommands'; 3 | 4 | const { onUserLogin, onUserLogout } = hx.authorize; 5 | 6 | export default function initLoginLogout(context: IContext) { 7 | onUserLogin(() => { 8 | console.warn('login'); 9 | refreshTree(); 10 | context.webviewProvider.refresh(); 11 | }); 12 | 13 | onUserLogout(() => { 14 | console.warn('logout'); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs') 2 | 3 | shell.pushd('./webviews') 4 | shell.exec('yarn') 5 | 6 | shell.popd() 7 | shell.exec('yarn') 8 | shell.exec('yarn build') 9 | 10 | shell.mkdir('-p', ['./temp/out', './temp/node_modules']) 11 | shell.cp('-Rf', './out/*', './temp/out/') 12 | 13 | shell.exec('yarn --prod=true') 14 | shell.cp('-Rf', './node_modules/*', './temp/node_modules/') 15 | 16 | shell.cp('./package.json', './temp/') 17 | shell.cp('./LICENSE', './temp/') 18 | -------------------------------------------------------------------------------- /src/utils/ip.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | export const getIp = () => { 4 | let address; 5 | const ifaces = os.networkInterfaces(); 6 | 7 | for (const dev in ifaces) { 8 | const iface = ifaces[dev]?.filter((details: os.NetworkInterfaceInfo) => { 9 | return details.family === 'IPv4' && details.internal === false; 10 | }); 11 | 12 | if (iface && iface.length > 0) { 13 | address = iface[0].address; 14 | } 15 | } 16 | 17 | return address; 18 | }; 19 | -------------------------------------------------------------------------------- /webviews/components/MergeRequestList/style.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 200px; 3 | } 4 | 5 | .block { 6 | margin-bottom: 20px; 7 | } 8 | 9 | .label { 10 | font-weight: 600; 11 | margin-bottom: 8px; 12 | } 13 | 14 | .item { 15 | padding: 5px 0; 16 | width: 100%; 17 | white-space: nowrap; 18 | text-overflow: ellipsis; 19 | overflow: hidden; 20 | cursor: pointer; 21 | } 22 | 23 | .item:hover, 24 | .item.selected { 25 | background-color: #06f; 26 | color: #fff; 27 | } 28 | -------------------------------------------------------------------------------- /src/init/initWorkspace.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import CodingServer from '../services/codingServer'; 3 | import ACTIONS, { dispatch } from '../utils/actions'; 4 | 5 | function initWorkspace(context: IContext) { 6 | hx.workspace.onDidChangeWorkspaceFolders(async function () { 7 | const repoInfo = await CodingServer.getRepoParams(); 8 | dispatch(ACTIONS.SET_REPO_INFO, { 9 | context, 10 | value: repoInfo, 11 | }); 12 | }); 13 | } 14 | 15 | export default initWorkspace; 16 | -------------------------------------------------------------------------------- /webviews/components/Actions/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDepot, refresh } from '../../utils/command'; 3 | 4 | const Actions = () => { 5 | const handleCreate = () => createDepot(); 6 | const handleRefresh = () => refresh(); 7 | 8 | return ( 9 |
10 |
创建仓库
11 |
刷新页面
12 |
13 | ); 14 | }; 15 | 16 | export default Actions; 17 | -------------------------------------------------------------------------------- /src/utils/toast.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | 3 | export const info = (msg: string, buttons?: string[]) => { 4 | return hx.window.showInformationMessage(msg, buttons); 5 | }; 6 | 7 | export const warn = (msg: string, buttons?: string[]) => { 8 | return hx.window.showWarningMessage(msg, buttons); 9 | }; 10 | 11 | export const error = (msg: string, buttons?: string[]) => { 12 | return hx.window.showErrorMessage(msg, buttons); 13 | }; 14 | 15 | export default { 16 | info, 17 | warn, 18 | error, 19 | }; 20 | -------------------------------------------------------------------------------- /src/init/registerCustomEditors.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import MRCustomEditorProvider from '../customEditors/mergeRequest'; 3 | import ACTIONS, { dispatch } from '../utils/actions'; 4 | 5 | function registerCustomEditors(context: IContext) { 6 | const mrCustomEditor = new MRCustomEditorProvider(context); 7 | hx.window.registerCustomEditorProvider('customEditor.mrDetail', mrCustomEditor); 8 | 9 | dispatch(ACTIONS.SET_MR_CUSTOM_EDITOR, { 10 | context, 11 | value: mrCustomEditor, 12 | }); 13 | } 14 | 15 | export default registerCustomEditors; 16 | -------------------------------------------------------------------------------- /src/initialize.ts: -------------------------------------------------------------------------------- 1 | import createTreeViews from './init/createTreeViews'; 2 | import createWebview from './init/createWebview'; 3 | import initWorkspace from './init/initWorkspace'; 4 | import registerCommands from './init/registerCommands'; 5 | import initLoginLogout from './init/initLoginLogout'; 6 | 7 | export function clear(context: IContext) { 8 | context.subscriptions.forEach(({ dispose }) => dispose()); 9 | } 10 | 11 | export default function initialize(context: IContext) { 12 | initLoginLogout(context); 13 | registerCommands(context); 14 | createTreeViews(context); 15 | createWebview(context); 16 | initWorkspace(context); 17 | } 18 | -------------------------------------------------------------------------------- /webviews/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | import { DataContext } from './reducers/context'; 6 | import appReducer, { initialState } from './reducers'; 7 | import './styles/common.css'; 8 | 9 | const Root = () => { 10 | const [state, dispatch] = useReducer(appReducer, initialState); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | ReactDOM.render( 20 | 21 | 22 | , 23 | document.getElementById('root'), 24 | ); 25 | -------------------------------------------------------------------------------- /src/typings/hbuilderx.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'hbuilderx' { 2 | const hx: any; 3 | export default hx; 4 | } 5 | 6 | interface IContext { 7 | id: string; 8 | dispose: () => void; 9 | subscriptions: any[]; 10 | ctx: { 11 | [key: string]: any; 12 | }; 13 | [key: string]: any; 14 | } 15 | 16 | interface IWebviewPanel { 17 | webView: any; 18 | postMessage: (message: any) => void; 19 | onDidDispose: (cb: () => void) => void; 20 | dispose: () => void; 21 | } 22 | 23 | interface ITreeItem { 24 | name?: string; 25 | children?: ITreeItem[]; 26 | } 27 | 28 | interface IQuickPickOption { 29 | label: string; 30 | description?: string; 31 | [key: string]: any; 32 | } 33 | -------------------------------------------------------------------------------- /.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: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | '@typescript-eslint/no-empty-function': 0, 20 | 'no-case-declarations': 0 21 | } 22 | }; -------------------------------------------------------------------------------- /webviews/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webviews", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack", 8 | "watch": "webpack --watch" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^0.21.0", 15 | "classnames": "^2.2.6", 16 | "querystring": "^0.2.0", 17 | "react": "^17.0.1", 18 | "react-dom": "^17.0.1", 19 | "react-select": "^3.1.1" 20 | }, 21 | "devDependencies": { 22 | "@types/classnames": "^2.2.11", 23 | "@types/react": "^16.9.56", 24 | "@types/react-dom": "^16.9.9", 25 | "@types/react-select": "^3.0.26" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /webviews/utils/command.ts: -------------------------------------------------------------------------------- 1 | export const toast = (msg: any) => { 2 | window.hbuilderx.postMessage({ 3 | command: 'webview.toast', 4 | data: msg 5 | }); 6 | }; 7 | 8 | export const createDepot = () => { 9 | window.hbuilderx.postMessage({ 10 | command: 'codingPlugin.createDepot', 11 | }); 12 | }; 13 | 14 | export const refresh = () => { 15 | window.hbuilderx.postMessage({ 16 | command: 'webview.refresh' 17 | }); 18 | }; 19 | 20 | export const auth = () => { 21 | window.hbuilderx.postMessage({ 22 | command: 'codingPlugin.auth' 23 | }); 24 | }; 25 | 26 | export const login = () => { 27 | window.hbuilderx.postMessage({ 28 | command: 'codingPlugin.login' 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /webviews/components/MergeRequestDetail/style.css: -------------------------------------------------------------------------------- 1 | .title { 2 | margin: 0 0 20px; 3 | font-size: 15px; 4 | font-weight: 600; 5 | } 6 | 7 | .status { 8 | display: inline-flex; 9 | align-items: center; 10 | justify-content: center; 11 | margin-left: 10px; 12 | padding: 3px 5px; 13 | border-radius: 2px; 14 | font-size: 13px; 15 | color: black; 16 | background-color: #e6e9ed; 17 | } 18 | 19 | .status.success { 20 | background-color: #bbfada; 21 | color: #08934d; 22 | } 23 | .status.error { 24 | background-color: #fed2d2; 25 | color: #ca1628; 26 | } 27 | .status.merged { 28 | background-color: #cceaff; 29 | } 30 | 31 | .link { 32 | font-size: 14px; 33 | font-weight: normal; 34 | margin-left: 15px; 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./", 4 | "baseUrl": "./", 5 | "outDir": "out/webviews", 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": [ 9 | "es6", 10 | "dom", 11 | "esnext.asynciterable" 12 | ], 13 | "sourceMap": false, 14 | "allowJs": true, 15 | "jsx": "react", 16 | "moduleResolution": "node", 17 | "forceConsistentCasingInFileNames": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noImplicitAny": true, 21 | "strictNullChecks": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "noUnusedLocals": true, 24 | "skipLibCheck": true, 25 | "allowSyntheticDefaultImports": true 26 | }, 27 | "include": [ 28 | "webviews", 29 | "types" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /docs/USAGE.md: -------------------------------------------------------------------------------- 1 | ## 安装 2 | 3 | [coding-hbuilderx 插件下载地址](https://ext.dcloud.net.cn/plugin?name=coding-hbuilderx) 4 | 5 | ## 使用方法 6 | 1. 在插件市场,下载安装插件; 7 | 2. 在 hbuilderx 编辑器,点击菜单【视图】-【插件扩展视图】,打开`CODING 代码仓库`视图; 8 | 3. 首次使用插件时,需要先登录 DCloud 账号,然后授权 CODING,以获取用户邮箱等信息; 9 | 4. 授权后即可使用插件(CODING 代码仓库相关功能)。 10 | 11 | ## 插件功能 12 | - 查看代码仓库列表; 13 | - 创建代码仓库; 14 | - 查看合并请求列表; 15 | - 查看合并请求概览; 16 | - 合并请求相关操作:合并/允许合并/撤销允许合并/关闭合并请求; 17 | - 将本地代码托管到 CODING。 18 | 19 | ## 界面预览 20 | ### 用户授权 CODING 代码仓库 21 | ![用户授权 CODING 代码仓库](https://codingcorp.coding.net/p/mo-test/d/statics/git/raw/master/2.png) 22 | 23 | ### 插件功能主界面 24 | ![插件功能主界面](https://codingcorp.coding.net/p/mo-test/d/statics/git/raw/master/5.png) 25 | 26 | ### 将本地代码托管到 CODING 27 | ![将本地代码托管到 CODING](https://codingcorp.coding.net/p/mo-test/d/statics/git/raw/master/7.png) 28 | -------------------------------------------------------------------------------- /src/utils/repo.ts: -------------------------------------------------------------------------------- 1 | import { IRepoInfo, IMRItem, IDepot } from '../typings/common'; 2 | 3 | export function parseCloneUrl(url: string): IRepoInfo | undefined { 4 | const reg = /^(https:\/\/|git@)e\.coding\.net(\/|:)(.*)\.git$/i; 5 | const result = url.match(reg); 6 | 7 | if (!result) { 8 | return; 9 | } 10 | 11 | const str = result.pop(); 12 | if (!str || !str?.includes(`/`)) { 13 | return; 14 | } 15 | 16 | const [team, project, repo] = str.split(`/`); 17 | return { team, project, repo: repo || project }; 18 | } 19 | 20 | export function getMRUrl(team: string, mrItem: IMRItem): string { 21 | return `https://${team}.coding.net${mrItem.path}`; 22 | } 23 | 24 | export function getDepotUrl(team: string, depot: IDepot): string { 25 | return `https://${team}.coding.net${depot.depotPath}`; 26 | } 27 | -------------------------------------------------------------------------------- /src/init/createTreeViews.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import DepotTreeDataProvider from '../trees/depot'; 3 | import MRTreeDataProvider from '../trees/mr'; 4 | import DepotMRTreeDataProvider from '../trees/depotMR'; 5 | 6 | const { createTreeView } = hx.window; 7 | 8 | const TREE_VIEWS = [ 9 | { 10 | id: 'codingPlugin.treeMR', 11 | treeDataProvider: MRTreeDataProvider, 12 | }, 13 | { 14 | id: 'codingPlugin.treeDepot', 15 | treeDataProvider: DepotTreeDataProvider, 16 | }, 17 | { 18 | id: 'codingPlugin.treeDepotMR', 19 | treeDataProvider: DepotMRTreeDataProvider, 20 | }, 21 | ]; 22 | 23 | function createTreeViews(context: IContext) { 24 | TREE_VIEWS.forEach(({ id, treeDataProvider }) => { 25 | context.subscriptions.push( 26 | createTreeView(id, { 27 | showCollapseAll: false, 28 | treeDataProvider: new treeDataProvider(context), 29 | }), 30 | ); 31 | }); 32 | } 33 | 34 | export default createTreeViews; 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { 6 | main: './webviews/index.tsx' 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, 'out/webviews'), 10 | filename: '[name].js', 11 | }, 12 | resolve: { 13 | extensions: ['.js', '.ts', '.tsx', '.json'], 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(ts|tsx)$/, 19 | loader: 'ts-loader', 20 | options: {}, 21 | }, 22 | { 23 | test: /\.css$/, 24 | use: [ 25 | { 26 | loader: 'style-loader', 27 | }, 28 | { 29 | loader: 'css-loader', 30 | options: { 31 | modules: { 32 | localIdentName: '[path][name]__[local]--[hash:base64:5]', 33 | }, 34 | }, 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | performance: { 41 | hints: false, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /webviews/typings/common.ts: -------------------------------------------------------------------------------- 1 | export interface IDepot { 2 | depotPath: string; 3 | gitHttpsHost: string; 4 | gitHttpsUrl: string; 5 | gitSshHost: string; 6 | gitSshUrl: string; 7 | id: number; 8 | isDefault: boolean; 9 | isSvnHttp: boolean; 10 | name: string; 11 | shared: boolean; 12 | size: number; 13 | status: number; 14 | svnEnabled: boolean; 15 | vcsType: 'git' | 'svn'; 16 | } 17 | 18 | export interface IUserInfo { 19 | id: number; 20 | avatar: string; 21 | global_key: string; 22 | name: string; 23 | path: string; 24 | team: string; 25 | } 26 | 27 | export interface IReviewer { 28 | reviewer: IUserInfo; 29 | value: number; 30 | volunteer: string; 31 | } 32 | 33 | export interface IMRItem { 34 | id: number; 35 | iid: number; 36 | srcBranch: string; 37 | desBranch: string; 38 | title: string; 39 | path: string; 40 | author: IUserInfo; 41 | reviewers: IReviewer[]; 42 | } 43 | 44 | export interface IRepoInfo { 45 | team: string; 46 | project: string; 47 | repo: string; 48 | } 49 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import initialize from './initialize'; 2 | import CodingServer from './services/codingServer'; 3 | import ACTIONS, { dispatch } from './utils/actions'; 4 | import { proxyCtx } from './utils/proxy'; 5 | import toast from './utils/toast'; 6 | import { readConfig } from './services/dcloud'; 7 | 8 | async function activate(context: IContext) { 9 | const codingServer = new CodingServer(context); 10 | const repoInfo = await CodingServer.getRepoParams(); 11 | console.warn('repoInfo: ', repoInfo); 12 | const token = await readConfig(`token`); 13 | 14 | dispatch(ACTIONS.SET_CTX, { 15 | context, 16 | value: { 17 | webviewProvider: null, 18 | codingServer, 19 | depots: [], 20 | selectedDepot: null, 21 | selectedMR: null, 22 | token, 23 | userInfo: null, 24 | repoInfo, 25 | }, 26 | }); 27 | 28 | proxyCtx(context); 29 | initialize(context); 30 | } 31 | 32 | function deactivate() { 33 | toast.info('plugin deactivate'); 34 | } 35 | 36 | export { activate, deactivate }; 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coding-hbuilderx 2 | 3 | ## HBuilderX插件通用注意事项 4 | 5 | HBuilderX-2.7.12以下版本安装插件市场内的插件后,卸载时需手动卸载,详细教程参考:[如何手动卸载插件](https://ask.dcloud.net.cn/article/37381) 6 | 7 | ## 安装 8 | 9 | [coding-hbuilderx 插件下载地址](https://ext.dcloud.net.cn/plugin?name=coding-hbuilderx) 10 | 11 | ## 使用方法 12 | 13 | 1. 在插件市场,下载安装插件; 14 | 1. 在 hbuilderx 编辑器,点击菜单【视图】-【插件扩展视图】,打开CODING 代码仓库视图; 15 | 1. 首次使用插件时,需要先登录 DCloud 账号,然后授权 CODING,以获取用户邮箱等信息; 16 | 1. 授权后即可使用插件(CODING 代码仓库相关功能)。 17 | 18 | ## 插件功能 19 | 20 | - 查看代码仓库列表; 21 | - 创建代码仓库; 22 | - 查看合并请求列表; 23 | - 查看合并请求概览; 24 | - 合并请求相关操作:合并/允许合并/撤销允许合并/关闭合并请求; 25 | - 将本地代码托管到 CODING。 26 | 27 | ## 界面预览 28 | 29 | ![用户授权 CODING 代码仓库](https://codingcorp.coding.net/p/mo-test/d/statics/git/raw/master/2.png) 30 | 31 | ![插件功能主界面](https://codingcorp.coding.net/p/mo-test/d/statics/git/raw/master/5.png) 32 | 33 | ![将本地代码托管到 CODING](https://codingcorp.coding.net/p/mo-test/d/statics/git/raw/master/7.png) 34 | 35 | ## Development 36 | 37 | 1. start dev mode. 38 | 39 | ```console 40 | yarn watch 41 | ``` 42 | 43 | 2. click run plugin option in hbuilder. 44 | -------------------------------------------------------------------------------- /webviews/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; 2 | import { toast } from './command'; 3 | 4 | const formatErrorMessage = (msg: string | Record) => { 5 | if (typeof msg === 'string') return msg; 6 | return Object.values(msg).join(); 7 | }; 8 | 9 | const handleResponse = (response: any) => { 10 | const result = response.data; 11 | 12 | if (result.code) { 13 | const message = formatErrorMessage(result.msg); 14 | toast(message); 15 | return result; 16 | } 17 | 18 | return result; 19 | }; 20 | 21 | const handleError = (error: any) => { 22 | const { response, message } = error; 23 | return Promise.reject(response ? new Error(response.data.message || message) : error); 24 | }; 25 | 26 | interface Instance extends AxiosInstance { 27 | (config: AxiosRequestConfig): Promise; 28 | } 29 | 30 | const createInstance = (): Instance => { 31 | const instance = axios.create(); 32 | instance.interceptors.response.use(handleResponse, handleError); 33 | return instance; 34 | }; 35 | 36 | export default createInstance(); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Al Cheung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/mr.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import os from 'os'; 3 | import { IRepoInfo, IDepot, IUserInfo } from '../typings/common'; 4 | import CodingServer from '../services/codingServer'; 5 | 6 | export const getMrListParams = async (selectedDepot: IDepot, user: IUserInfo): Promise => { 7 | const team = user.team; 8 | 9 | if (selectedDepot) { 10 | const { depotPath } = selectedDepot; 11 | const matchRes = depotPath.match(/\/p\/([^/]+)\/d\/([^/]+)\//); 12 | if (matchRes) { 13 | const [, project, repo] = matchRes; 14 | return { team, project, repo }; 15 | } 16 | } 17 | 18 | const result = await CodingServer.getRepoParams(); 19 | return result; 20 | }; 21 | 22 | export const getHostsPath = () => { 23 | const operatingSystem = os.platform(); 24 | let file; 25 | switch (operatingSystem) { 26 | case 'win32': 27 | break; 28 | case 'darwin': 29 | default: 30 | file = '/etc/hosts'; 31 | } 32 | return file; 33 | }; 34 | 35 | export const openHosts = () => { 36 | const hostsPath = getHostsPath(); 37 | hx.workspace.openTextDocument(hostsPath); 38 | }; 39 | -------------------------------------------------------------------------------- /webviews/styles/common.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: "PingFang SC", "Helvetica Neue", "Hiragino Sans GB", "Segoe UI", "Microsoft YaHei", 微软雅黑, sans-serif; 5 | } 6 | 7 | a { 8 | cursor: pointer; 9 | color: #06f; 10 | } 11 | 12 | a:hover { 13 | text-decoration: underline; 14 | } 15 | 16 | :global(.themeLight) { 17 | background-color: #fffae8; 18 | color: black; 19 | } 20 | 21 | :global(.themeDark) { 22 | background-color: #272822; 23 | color: white; 24 | } 25 | 26 | :global(.themeDarkBlue) { 27 | background-color: #282c35; 28 | color: white; 29 | } 30 | 31 | :global(.title) { 32 | margin: 20px 0 15px; 33 | padding-bottom: 5px; 34 | font-size: 18px; 35 | font-weight: 600; 36 | border-bottom: 1px solid #ccc; 37 | } 38 | 39 | :global(.btnGroup) { 40 | display: flex; 41 | align-items: center; 42 | } 43 | 44 | :global(.btn) { 45 | display: inline-block; 46 | margin-right: 10px; 47 | padding: 5px 10px; 48 | border-radius: 3px; 49 | border: 1px solid #ccc; 50 | cursor: pointer; 51 | } 52 | 53 | :global(.btn):hover { 54 | opacity: 0.8; 55 | } 56 | 57 | :global(.disabled) { 58 | cursor: not-allowed; 59 | opacity: 0.5; 60 | } 61 | 62 | :global(.btnPrimary) { 63 | border: none; 64 | background-color: #06f; 65 | color: #fff; 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosInstance } from 'axios'; 2 | import toast from './toast'; 3 | 4 | const formatErrorMessage = (msg: string | Record) => { 5 | if (typeof msg === 'string') return msg; 6 | return Object.values(msg).join(); 7 | }; 8 | 9 | const handleResponse = (response: any) => { 10 | const result = response.data; 11 | 12 | if (result.code) { 13 | const message = formatErrorMessage(result.msg); 14 | toast.error(message); 15 | console.error(`${response.config.url}:`, result); 16 | return result; 17 | } 18 | 19 | return result; 20 | }; 21 | 22 | const handleError = (error: any) => { 23 | const { response, message } = error; 24 | return Promise.reject(response ? new Error(response.data.message || message) : error); 25 | }; 26 | 27 | interface Instance extends AxiosInstance { 28 | (config: AxiosRequestConfig): Promise; 29 | } 30 | 31 | const createInstance = (): Instance => { 32 | const instance = axios.create(); 33 | instance.interceptors.response.use(handleResponse, handleError); 34 | return instance; 35 | }; 36 | 37 | const isProd = true; 38 | 39 | export const getApiPrefix = (team = 'e') => { 40 | return isProd ? `https://${team}.coding.net` : `http://${team}.staging-corp.coding.io`; 41 | }; 42 | 43 | export default createInstance(); 44 | -------------------------------------------------------------------------------- /src/utils/actions.ts: -------------------------------------------------------------------------------- 1 | import { setConfig } from '../services/dcloud'; 2 | 3 | const enum ACTIONS { 4 | SET_DEPOTS = 'SET_DEPOTS', 5 | SET_SELECTED_DEPOT = 'SET_SELECTED_DEPOT', 6 | SET_SELECTED_MR = 'SET_SELECTED_MR', 7 | SET_USER_INFO = 'SET_USER_INFO', 8 | SET_REPO_INFO = 'SET_REPO_INFO', 9 | SET_CTX = 'SET_CTX', 10 | SET_MR_CUSTOM_EDITOR = 'SET_MR_CUSTOM_EDITOR', 11 | SET_TOKEN = 'SET_TOKEN', 12 | SET_WEBVIEW = 'SET_WEBVIEW', 13 | } 14 | 15 | interface IPayload { 16 | context: IContext; 17 | value: any; 18 | } 19 | 20 | export const dispatch = (type: ACTIONS, { context, value }: IPayload) => { 21 | switch (type) { 22 | case ACTIONS.SET_DEPOTS: 23 | context.depots = value; 24 | break; 25 | case ACTIONS.SET_SELECTED_DEPOT: 26 | context.selectedDepot = value; 27 | break; 28 | case ACTIONS.SET_SELECTED_MR: 29 | context.selectedMR = value; 30 | break; 31 | case ACTIONS.SET_USER_INFO: 32 | context.userInfo = value; 33 | break; 34 | case ACTIONS.SET_REPO_INFO: 35 | context.repoInfo = value; 36 | break; 37 | case ACTIONS.SET_CTX: 38 | context.ctx = value; 39 | break; 40 | case ACTIONS.SET_MR_CUSTOM_EDITOR: 41 | context.mrCustomEditor = value; 42 | break; 43 | case ACTIONS.SET_TOKEN: 44 | context.token = value; 45 | setConfig(`token`, value); 46 | break; 47 | case ACTIONS.SET_WEBVIEW: 48 | context.webviewProvider = value; 49 | break; 50 | default: 51 | return; 52 | } 53 | }; 54 | 55 | export default ACTIONS; 56 | -------------------------------------------------------------------------------- /src/typings/common.ts: -------------------------------------------------------------------------------- 1 | export interface IRepoInfo { 2 | team: string; 3 | project: string; 4 | repo: string; 5 | } 6 | 7 | export interface IUserInfo { 8 | id: number; 9 | avatar: string; 10 | global_key: string; 11 | name: string; 12 | path: string; 13 | team: string; 14 | } 15 | 16 | export interface IReviewer { 17 | reviewer: IUserInfo; 18 | value: number; 19 | volunteer: string; 20 | } 21 | 22 | export interface IMRItem { 23 | id: number; 24 | iid: number; 25 | srcBranch: string; 26 | desBranch: string; 27 | title: string; 28 | path: string; 29 | author: IUserInfo; 30 | reviewers: IReviewer[]; 31 | } 32 | 33 | export interface IDepot { 34 | depotPath: string; 35 | gitHttpsHost: string; 36 | gitHttpsUrl: string; 37 | gitSshHost: string; 38 | gitSshUrl: string; 39 | id: number; 40 | isDefault: boolean; 41 | isSvnHttp: boolean; 42 | name: string; 43 | shared: boolean; 44 | size: number; 45 | status: number; 46 | svnEnabled: boolean; 47 | vcsType: 'git' | 'svn'; 48 | } 49 | 50 | export interface IOAuthResponse { 51 | code: string; 52 | error: number; 53 | } 54 | 55 | export interface ITokenResponse { 56 | ret: number; 57 | desc: string; 58 | data: { 59 | access_token: string; 60 | access_token_ttl: string; 61 | refresh_token: string; 62 | refresh_token_ttl: string; 63 | }; 64 | } 65 | 66 | export interface IDCloudUser { 67 | ret: number; 68 | desc: string; 69 | data: { 70 | nickname: string; 71 | avatar: string; 72 | uid: string; 73 | email: string; 74 | phone: string; 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/init/initCredentials.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import * as DCloudService from '../services/dcloud'; 3 | import toast from '../utils/toast'; 4 | import { refreshTree } from './registerCommands'; 5 | import ACTIONS, { dispatch } from '../utils/actions'; 6 | 7 | const { executeCommand } = hx.commands; 8 | 9 | export async function initCredentials(context: IContext, callback?: () => void) { 10 | try { 11 | let hbToken = await DCloudService.readConfig(`hbToken`); 12 | const token = await DCloudService.readConfig(`token`); 13 | 14 | if (!hbToken) { 15 | const code = await DCloudService.grantForUserInfo(); 16 | const tokenResult = await DCloudService.applyForToken(code); 17 | hbToken = tokenResult.data.access_token; 18 | } 19 | 20 | const resp = await DCloudService.fetchUser(hbToken); 21 | console.warn(`logged in as DCloud user: ${resp.data.nickname} ${resp.data.email}`); 22 | 23 | if (!token) { 24 | await executeCommand('codingPlugin.createTeam'); 25 | } 26 | 27 | const { 28 | ctx: { codingServer, token: accessToken }, 29 | } = context; 30 | if (accessToken) { 31 | const userData = await codingServer.getUserInfo(accessToken); 32 | toast.info(`logged in as CODING user: ${userData.name} @ ${userData.team}`); 33 | 34 | dispatch(ACTIONS.SET_USER_INFO, { 35 | context: context, 36 | value: userData, 37 | }); 38 | } 39 | 40 | if (callback) { 41 | callback(); 42 | } 43 | } catch (err) { 44 | if (Number(err) === 1) { 45 | toast.warn(`请先登录 DCloud`); 46 | } 47 | } finally { 48 | refreshTree(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/trees/depot.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import ACTIONS, { dispatch } from '../utils/actions'; 3 | import toast from '../utils/toast'; 4 | import { IDepot } from '../typings/common'; 5 | 6 | interface IItem extends ITreeItem { 7 | _create: boolean; 8 | _auth: boolean; 9 | } 10 | 11 | const getCommand = (element: IDepot & IItem) => { 12 | if (element.children) return ''; 13 | if (element._auth) return 'codingPlugin.auth'; 14 | if (element._create) return 'codingPlugin.createDepot'; 15 | return 'codingPlugin.depotTreeItemClick'; 16 | }; 17 | 18 | class DepotTreeDataProvider extends hx.TreeDataProvider { 19 | constructor(context: IContext) { 20 | super(); 21 | this.context = context; 22 | } 23 | 24 | getUser() { 25 | return this.context.codingServer.session?.user; 26 | } 27 | 28 | async getChildren(element: IDepot & IItem) { 29 | const user = this.getUser(); 30 | if (!user) { 31 | toast.warn('请先绑定 CODING 账户'); 32 | return Promise.resolve([ 33 | { 34 | name: '绑定 CODING 账户', 35 | _auth: true, 36 | }, 37 | ]); 38 | } 39 | 40 | if (element) { 41 | return Promise.resolve(element.children); 42 | } 43 | 44 | try { 45 | const depots = await this.context.codingServer.getDepotList(); 46 | 47 | dispatch(ACTIONS.SET_DEPOTS, { 48 | context: this.context, 49 | value: depots, 50 | }); 51 | 52 | return Promise.resolve([ 53 | { 54 | name: '+ 创建仓库', 55 | _create: true, 56 | }, 57 | ...depots, 58 | ]); 59 | } catch { 60 | toast.error('获取仓库列表失败'); 61 | } 62 | } 63 | 64 | getTreeItem(element: IDepot & IItem) { 65 | return { 66 | label: element.name, 67 | collapsibleState: element.children ? 1 : 0, 68 | command: { 69 | command: getCommand(element), 70 | arguments: element, 71 | }, 72 | contextValue: 'createDepot', 73 | }; 74 | } 75 | } 76 | 77 | export default DepotTreeDataProvider; 78 | -------------------------------------------------------------------------------- /webviews/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { getDepotProject } from '../utils/depot'; 2 | import { IDepot, IMRItem, IRepoInfo } from '../typings/common'; 3 | 4 | export interface IState { 5 | token: string; 6 | userInfo: Record; 7 | repoInfo: IRepoInfo | null; 8 | refetchDepotList: boolean; 9 | selectedDepot: IDepot | null; 10 | selectedProjectName: string | undefined; 11 | selectedMR: IMRItem | null; 12 | } 13 | 14 | export interface IAction { 15 | type: ACTIONS; 16 | payload: any; 17 | } 18 | 19 | export const enum ACTIONS { 20 | REFETCH_DEPOT_LIST = 'REFETCH_DEPOT_LIST', 21 | SET_SELECTED_DEPOT = 'SET_SELECTED_DEPOT', 22 | SET_SELECTED_MR = 'SET_SELECTED_MR', 23 | } 24 | 25 | const getInitialState = (): IState => { 26 | const CODING_DATA = JSON.parse(window.__CODING__); 27 | const { repoInfo, userInfo } = CODING_DATA; 28 | const selectedDepot = repoInfo && userInfo && repoInfo.team === userInfo.team 29 | ? { 30 | name: repoInfo.repo, 31 | depotPath: `/p/${repoInfo.project}/d/${repoInfo.repo}/git`, 32 | vcsType: 'git' 33 | } 34 | : null; 35 | 36 | return { 37 | ...CODING_DATA, 38 | refetchDepotList: false, 39 | selectedDepot, 40 | selectedProjectName: repoInfo ? repoInfo.project : '', 41 | selectedMR: null 42 | }; 43 | }; 44 | 45 | export const initialState = getInitialState(); 46 | 47 | const appReducer = (state: IState, { type, payload }: IAction) => { 48 | switch (type) { 49 | case ACTIONS.REFETCH_DEPOT_LIST: 50 | return { 51 | ...state, 52 | refetchDepotList: payload 53 | }; 54 | case ACTIONS.SET_SELECTED_DEPOT: 55 | return { 56 | ...state, 57 | selectedDepot: payload, 58 | selectedProjectName: getDepotProject(payload.depotPath), 59 | selectedMR: null 60 | }; 61 | case ACTIONS.SET_SELECTED_MR: 62 | return { 63 | ...state, 64 | selectedMR: payload 65 | }; 66 | default: 67 | } 68 | return state; 69 | }; 70 | 71 | export default appReducer; 72 | -------------------------------------------------------------------------------- /webviews/hooks/useAsyncFn.ts: -------------------------------------------------------------------------------- 1 | import { DependencyList, useCallback, useState, useRef, useEffect } from 'react'; 2 | import useMountedState from './useMountedState'; 3 | 4 | interface IError { 5 | code: number; 6 | msg: { 7 | [key: string]: string; 8 | }; 9 | } 10 | 11 | export interface AsyncState { 12 | loading: boolean; 13 | error?: IError; 14 | value?: any; 15 | } 16 | 17 | export type AsyncFn = [AsyncState, (...args: any[]) => Promise]; 18 | 19 | export interface IOptions { 20 | deps: DependencyList; 21 | initialState?: AsyncState; 22 | successHandler?: (value: any) => void; 23 | errorHandler?: (error: IError) => void; 24 | } 25 | 26 | /* tslint:disable */ 27 | export default function useAsyncFn( 28 | fn: (...args: any[]) => Promise, 29 | options: IOptions = { 30 | deps: [], 31 | } 32 | ): AsyncFn { 33 | const { initialState = { loading: false }, deps = [], successHandler, errorHandler } = options; 34 | 35 | const lastCallId = useRef(0); 36 | const [state, set] = useState(initialState); 37 | 38 | const isMounted = useMountedState(); 39 | 40 | useEffect(() => { 41 | set(initialState); 42 | }, deps); 43 | 44 | const callback = useCallback((...args: any[]) => { 45 | const callId = ++lastCallId.current; 46 | set({ loading: true, error: undefined }); 47 | 48 | return fn(...args).then( 49 | (value) => { 50 | const cb = args[args.length - 1]; 51 | 52 | if (isMounted() && callId === lastCallId.current) { 53 | if (successHandler) successHandler(value); 54 | if (typeof cb === 'function') { 55 | cb(); 56 | } 57 | set({ value, loading: false }); 58 | } 59 | return value; 60 | }, 61 | (error) => { 62 | if (isMounted() && callId === lastCallId.current) { 63 | if (errorHandler) errorHandler(error); 64 | set({ error, loading: false }); 65 | } 66 | return null; 67 | } 68 | ); 69 | }, deps); 70 | 71 | return [state, callback]; 72 | } 73 | -------------------------------------------------------------------------------- /src/webviews/index.ts: -------------------------------------------------------------------------------- 1 | import hx from 'hbuilderx'; 2 | import path from 'path'; 3 | import toast from '../utils/toast'; 4 | 5 | interface IMessage { 6 | command: string; 7 | data: any; 8 | } 9 | 10 | export default class WebviewProvider { 11 | context: IContext; 12 | panel: IWebviewPanel; 13 | 14 | constructor(context: IContext) { 15 | this.context = context; 16 | this.panel = this.createPanel(); 17 | this.listen(); 18 | } 19 | 20 | listen() { 21 | this.panel.webView.onDidReceiveMessage((message: IMessage) => { 22 | console.log('webview receive message => ', message); 23 | const { command, data } = message; 24 | 25 | switch (command) { 26 | case 'webview.goToPage': 27 | hx.env.openExternal(data); 28 | break; 29 | case 'webview.toast': 30 | toast.error(data); 31 | break; 32 | default: 33 | hx.commands.executeCommand(command); 34 | return; 35 | } 36 | }); 37 | } 38 | 39 | createPanel() { 40 | const webviewPanel: IWebviewPanel = hx.window.createWebView('codingPlugin.webview', { 41 | enableScripts: true, 42 | }); 43 | 44 | return webviewPanel; 45 | } 46 | 47 | update(data: any) { 48 | const webview = this.panel.webView; 49 | const fileInfo = hx.Uri.file(path.resolve(__dirname, '../../out/webviews/main.js')); 50 | 51 | const config = hx.workspace.getConfiguration(); 52 | const colorScheme = config.get('editor.colorScheme'); 53 | 54 | const COLORS: Record = { 55 | Monokai: 'themeDark', 56 | 'Atom One Dark': 'themeDarkBlue', 57 | Default: 'themeLight', 58 | }; 59 | 60 | webview.html = ` 61 | 62 | 63 |
64 |
65 |
66 | 69 | 70 | 71 | 72 | `; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /webviews/components/DepotSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from 'react'; 2 | import Select from 'react-select'; 3 | 4 | import { DataContext } from '../../reducers/context'; 5 | import { ACTIONS } from '../../reducers'; 6 | import { getDepotList } from '../../services'; 7 | import useAsyncFn from '../../hooks/useAsyncFn'; 8 | import { IDepot } from '../../typings/common'; 9 | import { getDepotProject } from '../../utils/depot'; 10 | import style from './style.css'; 11 | 12 | const DepotSelect = () => { 13 | const { state, dispatch } = useContext(DataContext); 14 | const { token, userInfo, refetchDepotList, selectedDepot } = state; 15 | 16 | const [getDepotListState, getDepotListFn] = useAsyncFn(getDepotList); 17 | const { loading, value: depotList = [] } = getDepotListState; 18 | 19 | const fetchData = async () => { 20 | try { 21 | await getDepotListFn(token, userInfo.team); 22 | dispatch({ 23 | type: ACTIONS.REFETCH_DEPOT_LIST, 24 | payload: false 25 | }); 26 | } catch (err) { 27 | alert(err); 28 | } 29 | }; 30 | 31 | useEffect(() => { 32 | fetchData(); 33 | }, []); 34 | 35 | useEffect(() => { 36 | if (refetchDepotList) { 37 | fetchData(); 38 | } 39 | }, [refetchDepotList]); 40 | 41 | const getOptionLabel = ({ name, depotPath }: IDepot) => { 42 | const project = getDepotProject(depotPath); 43 | return `${project}/${name}`; 44 | }; 45 | 46 | return ( 47 |
48 | 当前仓库: 49 |
50 |