diff --git a/.babelrc b/.babelrc index 1500fe9a..170bd0bd 100644 --- a/.babelrc +++ b/.babelrc @@ -1,93 +1,37 @@ { "presets": [ - ["env", {"modules": false}], - "react", - "stage-0" + ["@babel/preset-env", {"modules": false, "useBuiltIns": "entry"}], + "@babel/preset-react", + ["@babel/preset-stage-0", { "decoratorsLegacy": true }] ], "plugins": [ - "transform-decorators-legacy" + ["@babel/plugin-transform-runtime", { + "helpers": false, + "polyfill": false, + "regenerator": true, + "moduleName": "@babel/runtime" + }], + "@babel/plugin-syntax-export-default-from", + "@babel/plugin-syntax-dynamic-import", + ["@babel/plugin-proposal-decorators", { "legacy": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }] ], "env": { + "production": { + "plugins": [ + "@babel/plugin-transform-react-constant-elements" + ] + }, "development": { "plugins": [ "react-hot-loader/babel" ] }, "test": { - "presets": ["es2015", "react", "stage-0"], + "presets": ["@babel/preset-es2015", "@babel/preset-react", "@babel/preset-stage-0"], "plugins": [ - "transform-decorators-legacy" + ["@babel/plugin-proposal-decorators", { "legacy": true }] ] } } } - -/* -Presets/Plugins order matters -[Ref](http://babeljs.io/docs/plugins/#plugin-preset-ordering) - -In short: -1. plugins run before presets -2. plugins run in top-down order -3. presets run in bottom-up order -*/ - -/* As of 2017-02-23, most plugins at semver "^6.22.0" -* Below are plugins that're included in aforementioned presets, in the order they're supposed to apply - -# presets stage-0 to stage-3: -## stage-0 -transform-do-expressions -transform-function-bind -## stage-1 -transform-class-constructor-call -transform-export-extensions -## stage-2 -transform-class-properties -transform-decorators -syntax-dynamic-import -## stage-3 -syntax-trailing-function-commas -transform-async-generator-functions -transform-async-to-generator -transform-exponentiation-operator -transform-object-rest-spread - -# preset react: -preset-flow -syntax-jsx -transform-react-jsx -transform-react-display-name - -* preset es2015, es2016, es2017: -## es2015 -check-es2015-constants -transform-es2015-arrow-functions -transform-es2015-block-scoped-functions -transform-es2015-block-scoping -transform-es2015-classes -transform-es2015-computed-properties -transform-es2015-destructuring -transform-es2015-duplicate-keys -transform-es2015-for-of -transform-es2015-function-name -transform-es2015-literals -transform-es2015-modules-amd -transform-es2015-modules-commonjs -transform-es2015-modules-systemjs -transform-es2015-modules-umd -transform-es2015-object-super -transform-es2015-parameters -transform-es2015-shorthand-properties -transform-es2015-spread -transform-es2015-sticky-regex -transform-es2015-template-literals -transform-es2015-typeof-symbol -transform-es2015-unicode-regex -transform-regenerator -## es2016 -transform-exponentiation-operator -## es2017 -syntax-trailing-function-commas -transform-async-to-generator -*/ diff --git a/.eslintignore b/.eslintignore index c45d403d..8b5e9dc4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules/* static/* *.spec.js +webpack_configs diff --git a/.eslintrc b/.eslintrc index d8c1b6e1..a5c1f7bc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -10,6 +10,7 @@ "no-param-reassign": 0, "new-cap": 0, "no-eval": 0, + "no-undef": 0, "no-plusplus": 0, "no-return-assign": 0, "no-underscore-dangle": 0, @@ -23,14 +24,16 @@ "named": "always", "asyncArrow": "ignore" }], + "react/prefer-stateless-function": 0, + "import/no-extraneous-dependencies": 0, + "import/no-unresolved": 0, "jsx-quotes": ["error", "prefer-single"], + "react/jsx-filename-extension": 0, "react/jsx-first-prop-new-line": 0, "react/forbid-prop-types": 0, + "import/extensions": 0, "jsx-a11y/no-static-element-interactions": 0 }, - "settings": { - "import/resolver": "webpack" - }, "globals": { "i18n": false, "__PACKAGE_PORTS__": false, diff --git a/.travis.yml b/.travis.yml index a5732cc6..fa452b7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ dist: trusty language: node_js node_js: -- '7' +- '10.14.1' cache: yarn diff --git a/404.html b/404.html new file mode 100644 index 00000000..9d2876db --- /dev/null +++ b/404.html @@ -0,0 +1,91 @@ + + + + + + 404 Page + + + + + +
+
+
404!
+

页面,我找不到你,我找不到你啊 ~

+ 返回首页 +
+
+
+ + diff --git a/500.html b/500.html new file mode 100644 index 00000000..4b350f16 --- /dev/null +++ b/500.html @@ -0,0 +1,91 @@ + + + + + + 500 Page + + + + + +
+
+
500!
+

抱歉,系统出错,正在抢修中~

+ 返回首页 +
+
+
+ + diff --git a/502.html b/502.html new file mode 100644 index 00000000..aa78df95 --- /dev/null +++ b/502.html @@ -0,0 +1,78 @@ + + + + + + 502 Page + + + + + +
+
+
502!
+
+

后端,我连不上你啊,我连不上你~

+

请在 Cloud Studio 中检查:

+

1、应用有没有监听访问链接生成的端口号。

+

2、您生成的链接是否已经是否。

+
+
+
+
+ + diff --git a/app/CodingSDK.js b/app/CodingSDK.js index 79cf504f..e72cac0c 100644 --- a/app/CodingSDK.js +++ b/app/CodingSDK.js @@ -10,15 +10,16 @@ import * as Modal from './components/Modal/actions' import * as SideBarActions from './components/Panel/SideBar/actions' import { notify, NOTIFY_TYPE } from './components/Notification/actions' import api from '../app/backendAPI' -import { closeWebsocketClient, closeTtySocketClient } from '../app/backendAPI/workspaceAPI' +import { closeWebsocketClient, closeTtySocketClient, closeSearchWebsocketClient } from '../app/backendAPI/workspaceAPI' import * as Panel from './components/Panel/actions' import * as File from './commons/File' import initializeState from './containers/Initialize/state' -import { app as appExports, lib as libExports } from './exports' +import { app as appExports, lib as libExports, cloudstudio } from './exports' window.app = appExports window.lib = libExports +window.cloudstudio = cloudstudio export default class { // app data @@ -61,7 +62,8 @@ export default class { get socketManager () { return ({ closeWebsocketClient, - closeTtySocketClient + closeTtySocketClient, + closeSearchWebsocketClient }) } diff --git a/app/account.html b/app/account.html new file mode 100644 index 00000000..c389bb43 --- /dev/null +++ b/app/account.html @@ -0,0 +1,16 @@ + + + + + + Cloud Studio - 开启云端开发模式 WebIDE + + + + + + +
+
+ + diff --git a/app/account.jsx b/app/account.jsx new file mode 100644 index 00000000..4da1937c --- /dev/null +++ b/app/account.jsx @@ -0,0 +1,23 @@ +import { AppContainer } from 'react-hot-loader' +import React from 'react' +import { render } from 'react-dom' +import Login from './containers/Root/account' +import './styles/main.styl' + +const rootElement = document.getElementById('root') +async function startApp (module) { + if (__DEV__) { + const hotLoaderRender = () => + render(, rootElement) + + hotLoaderRender() + if (module.hot) module.hot.accept('./containers/Login', hotLoaderRender) + } else { + render(, rootElement) + } +} + +startApp(module) + +const log = (...args) => console.log(...args) || (x => x) +if (__VERSION__) log(`[VERSION] ${__VERSION__}`) diff --git a/app/backendAPI/fileAPI.js b/app/backendAPI/fileAPI.js index 14afbc83..8f20854d 100644 --- a/app/backendAPI/fileAPI.js +++ b/app/backendAPI/fileAPI.js @@ -1,23 +1,41 @@ import { request, qs } from '../utils' -import config from '../config' import axios from 'axios' +import * as maskActions from 'components/Mask/actions' export function fetchPath (path, order, group) { return request.get(`/workspaces/${config.spaceKey}/files`, { path, order: true, group: true + }).then((res) => { + return res.filter((item) => { + return !item.name.startsWith('.nfs000') + }) }) } export function downloadFile (path, shouldPacked) { + let filePath = path || config.projectName || 'Home' + if (shouldPacked) { + filePath += '.tar.gz' + } + // if (config.isDefault) { + // filePath = 'Home.tar.gz' + // } const packOrRaw = shouldPacked ? 'pack' : 'raw' let url = `${config.baseURL}/workspaces/${config.spaceKey}/${packOrRaw}` url += `?${qs.stringify({ path, inline: false })}` - window.open(url, '_blank') + // window.open(url, '_blank') + const downloadTimeout = setTimeout(() => { + maskActions.showMask({ message: i18n`file.preparingDownload`, countdown: 60 }) + }, 600) + request.download(url, filePath.split('/').pop()).then((res) => { + clearTimeout(downloadTimeout) + maskActions.hideMask() + }) } export function uploadFile (path, file, option) { @@ -111,3 +129,11 @@ export function searchFile (value, includeNonProjectItems = false) { } }) } + +export function searchTxt (keyword) { + return request.post(`/workspaces/${config.spaceKey}/txt-search`, { keyword }, { + headers: { + 'Content-Type': 'application/json', + } + }) +} diff --git a/app/backendAPI/gitAPI.js b/app/backendAPI/gitAPI.js index acc1fe47..e1290a7f 100644 --- a/app/backendAPI/gitAPI.js +++ b/app/backendAPI/gitAPI.js @@ -26,10 +26,7 @@ export function gitPull () { } export function gitPushAll () { - return request.post(`/git/${config.spaceKey}/push?all=true`).then((res) => { - if (res.ok || res.nothingToPush) return true - if (!res.ok) return false - }) + return request.post(`/git/${config.spaceKey}/push?all=true`) } export function gitFetch () { return request.post(`/git/${config.spaceKey}/fetch`) @@ -139,9 +136,23 @@ export function gitLogs (params = {}) { parentIds: c.parents, message: c.shortMessage, date: new Date(c.commitTime * 1000), + shortId: c.shortName, }))) + .catch(() => {}) } export function gitRefs () { return request.get(`/git/${config.spaceKey}/refs`) } + +export function gitClone (data) { + return request.post('ws/clone', data, { headers: { Accept: 'application/vnd.coding.v1+json' } }); +} + +export function gitInit () { + return request.post(`/git/${config.spaceKey}/init`) +} + +export function gitRemote (url) { + return request.post(`/git/${config.spaceKey}/remote`, { url }) +} diff --git a/app/backendAPI/index.js b/app/backendAPI/index.js index ddbdba5b..22bb2c06 100644 --- a/app/backendAPI/index.js +++ b/app/backendAPI/index.js @@ -3,12 +3,18 @@ import * as gitAPI from './gitAPI' import * as packageAPI from './packageAPI' import * as workspaceAPI from './workspaceAPI' import * as websocketClients from './websocketClients' +import * as userAPI from './userAPI' +import * as projectAPI from './projectAPI' +import * as projectSettingAPI from './projectSettingApi' export default { ...fileAPI, ...gitAPI, ...packageAPI, - ...workspaceAPI + ...workspaceAPI, + ...userAPI, + ...projectAPI, + ...projectSettingAPI, } export { websocketClients } diff --git a/app/backendAPI/languageServerAPI.js b/app/backendAPI/languageServerAPI.js new file mode 100644 index 00000000..0a0f1797 --- /dev/null +++ b/app/backendAPI/languageServerAPI.js @@ -0,0 +1,10 @@ +import { request } from 'utils' +import config from 'config' + +export function fetchLanguageServerSetting (spaceKey) { + return request.get(`/settings/${spaceKey}/language`) +} + +export function setLanguageServerOne ({ type, srcPath }) { + return request.put(`/settings/${config.spaceKey}/language/one`, { type, srcPath }) +} diff --git a/app/backendAPI/packageAPI.js b/app/backendAPI/packageAPI.js index 5de65c09..a8d199e4 100644 --- a/app/backendAPI/packageAPI.js +++ b/app/backendAPI/packageAPI.js @@ -6,7 +6,6 @@ import config from '../config' const { packageServer, packageDev } = config - const io = require('socket.io-client/dist/socket.io.min.js') export const fetchPackageList = (type) => { @@ -21,30 +20,53 @@ export const fetchPackageList = (type) => { } export const fetchPackageInfo = (pkgName, pkgVersion, target) => - axios.get(`${target || packageServer}/packages/${pkgName}/${pkgVersion}/manifest.json`).then(res => res.data) + axios + .get(`${target || packageServer}/packages/${pkgName}/${pkgVersion}/manifest.json`) + .then(res => res.data) export const fetchPackageScript = (props) => { if (Array.isArray(props)) { - const concatedUrl = props.reduce((p, v, i) => `${p}${v.pkgName}/${v.pkgVersion}/index.js${i !== props.length - 1 ? ',' : ''}`, '??') + const concatedUrl = props.reduce( + (p, v, i) => `${p}${v.pkgName}/${v.pkgVersion}/index.js${i !== props.length - 1 ? ',' : ''}`, + '??' + ) + if (!config.packageDev && config.isPlatform) { + return axios.get(`${window.location.origin}/packages/${concatedUrl}`).then(res => res.data) + } return axios.get(`${packageServer}/packages/${concatedUrl}`).then(res => res.data) } - return axios.get(`${props.target || packageServer}/packages/${props.pkgName}/${props.pkgVersion}/index.js`).then(res => res.data) + return axios + .get(`${props.target || packageServer}/packages/${props.pkgName}/${props.pkgVersion}/index.js`) + .then(res => res.data) } export const enablePackageHotReload = (target) => { - const socket = io.connect(target || packageServer, { - reconnection: true, - reconnectionDelay: 1500, - reconnectionDelayMax: 10000, - reconnectionAttempts: 5, - transports: ['websocket'] - }) + const socket = io.connect( + target || packageServer, + { + reconnection: true, + reconnectionDelay: 1500, + reconnectionDelayMax: 10000, + reconnectionAttempts: 5, + transports: ['websocket'] + } + ) socket.on('change', (data) => { if (!data) return if (target) { - console.log(`plugin is reloading from ${target}`, data) + console.log(`[Package Hot Reload] Plugin is reloading from ${target}.`, data) data.codingIdePackage.TARGET = target } fetchPackage(data.codingIdePackage, 'reload') }) } + +export const fetchUserPackagelist = () => request.get('/user-plugin/enable/list') + +export const fetchUserPackageScript = url => axios.get(url).then(res => res.data) + +export const fethUserPreDeployPlugins = () => request.get('/user-plugin/pre/deploy/list') + +export const fetchPackageInfomation = () => request.get(`/user-plugin/info/${config.spaceKey}`) + +export const getMyPlugin = () => request.get('/user-plugin/dev/list?page=0&size=100') diff --git a/app/backendAPI/pluginAPI.js b/app/backendAPI/pluginAPI.js new file mode 100644 index 00000000..1adcaa10 --- /dev/null +++ b/app/backendAPI/pluginAPI.js @@ -0,0 +1,5 @@ +import { request, qs } from '../utils' + +export function fetchPackageHMRModule (packagename, version) { + return request(`/tty/${config.shardingGroup}/${config.spaceKey}/connect-other-service/packages/${packagename}/${version}`) +} diff --git a/app/backendAPI/projectAPI.js b/app/backendAPI/projectAPI.js new file mode 100644 index 00000000..1e2abdaf --- /dev/null +++ b/app/backendAPI/projectAPI.js @@ -0,0 +1,34 @@ +import { request } from '../utils' +import config from '../config' + +export function fetchProjects () { + return request.get('/projects') +} + +export function fetchTemplates () { + return request.get('/projects?template=true') +} + +export function findCodingProject ({ projectName, ownerName }) { + return request.get(`ws/find/coding/${ownerName}/${projectName}`, null, { headers: { Accept: '*/*' } }) +} + +export function findExternalProject ({ url }) { + return request.get(`ws/find/coding/${config.globalKey}`, { url }) +} + +export function syncProject () { + return request.post('/project/sync') +} + +export function queryCodingProject ({ source = 'coding', projectName }) { + return request.get(`/queryCodingProject`, { source, projectName }) +} + +export function showPublicSshKey () { + return request.get('user/public_key', null, { headers: { Accept: 'application/vnd.coding.v1+json' } }); +} + +export function createProject (options) { + return request.post('/projects', options, { headers: { Accept: '*/*' } }); +} diff --git a/app/backendAPI/projectSettingApi.js b/app/backendAPI/projectSettingApi.js new file mode 100644 index 00000000..7601f218 --- /dev/null +++ b/app/backendAPI/projectSettingApi.js @@ -0,0 +1,26 @@ +import request from 'utils/request' +import config from 'config' + +export function fetchProjectType () { + return request.get(`/ws/${config.spaceKey}/type`) +} + +export function putProjectType (projectConfigDto) { + return request.put(`/ws/${config.spaceKey}/type`, projectConfigDto, { + headers: { + 'Content-Type': 'application/json', + } + }) +} + +export function fetchClasspath () { + return request.get(`/ws/${config.spaceKey}/classpath`) +} + +export function postClasspath (classpath) { + return request.post(`/ws/${config.spaceKey}/classpath`, classpath, { + headers: { + 'Content-Type': 'application/json', + } + }) +} diff --git a/app/backendAPI/searchAPI.js b/app/backendAPI/searchAPI.js new file mode 100644 index 00000000..23e0aaee --- /dev/null +++ b/app/backendAPI/searchAPI.js @@ -0,0 +1,26 @@ +import { SearchSocketClient } from './websocketClients' +import config from 'config'; + +export function searchWorkspaceUp() { + SearchSocketClient.$$singleton.send(`/app/ws/up`, {spaceKey: config.spaceKey}); +} + +export function searchWorkspaceDown() { + SearchSocketClient.$$singleton.send(`/app/ws/down`, {spaceKey: config.spaceKey}); +} + +export function searchWorkspaceStatus() { + SearchSocketClient.$$singleton.send(`/app/ws/status`, {spaceKey: config.spaceKey}); +} + +export function searchString(searching, randomKey) { + SearchSocketClient.$$singleton.send(`/app/search/string`, {spaceKey: config.spaceKey, randomKey: randomKey}, JSON.stringify(searching)); +} + +export function searchPattern(searching, randomKey) { + SearchSocketClient.$$singleton.send(`/app/search/pattern`, {spaceKey: config.spaceKey, randomKey: randomKey}, JSON.stringify(searching)); +} + +export function searchInterrupt(taskId) { + SearchSocketClient.$$singleton.send(`/app/search/interrupt`, {spaceKey: config.spaceKey}, taskId); +} \ No newline at end of file diff --git a/app/backendAPI/userAPI.js b/app/backendAPI/userAPI.js new file mode 100644 index 00000000..7a2b8629 --- /dev/null +++ b/app/backendAPI/userAPI.js @@ -0,0 +1,46 @@ +import config from '../config' +import { request } from '../utils' + +export function hasCaptcha () { + return request.get('/login/captcha', null, + { headers: { Accept: '*/*' } } + ) +} + +export function login ({ + password, + email, + captcha, + remember_me +}) { + return request.post(`/login${location.search ? location.search : '?return_url=/dashboard'}`, { + password, + email, + captcha, + remember_me + }) +} + +// 两部验证 +export function loginCode ({ + code +}) { + return request.post(`/login${location.search ? location.search : '?return_url=/dashboard'}`, { + code + }) +} + +export function signout () { + return request.get('/logout') +} + +export function getUserProfile () { + // @fixme: initialize2 requires removing .then(res => res.data) + return request.get('/user/current', null, + { headers: { Accept: '*/*' } } + ) +} + +export function bindQcloud (data) { + return request.post(`/oauth/qcloud/bind_with_authentication/`, data) +} diff --git a/app/backendAPI/websocketClients.js b/app/backendAPI/websocketClients.js index d965f9cf..f0f8c15b 100644 --- a/app/backendAPI/websocketClients.js +++ b/app/backendAPI/websocketClients.js @@ -4,26 +4,30 @@ import getBackoff from 'utils/getBackoff' import emitter, * as E from 'utils/emitter' import config from 'config' import { autorun, runInAction } from 'mobx' -import { notify, NOTIFY_TYPE } from '../components/Notification/actions' +import notification from '../components/Notification' + const log = console.log || (x => x) const warn = console.warn || (x => x) -const io = require(__RUN_MODE__ ? 'socket.io-client/dist/socket.io.min.js' : 'socket.io-client-legacy/dist/socket.io.min.js') +const io = require(__RUN_MODE__ + ? 'socket.io-client/dist/socket.io.min.js' + : 'socket.io-client-legacy/dist/socket.io.min.js') class FsSocketClient { constructor () { if (FsSocketClient.$$singleton) return FsSocketClient.$$singleton - const url = config.isPlatform ? - `${config.wsURL}/sockjs/${config.spaceKey}` - : `${config.baseURL}/sockjs/` + const url = config.isPlatform + ? `${config.wsURL}/sockjs/${config.spaceKey}` + : `${config.baseURL}/sockjs/` // SockJS auto connects at initiation this.sockJSConfigs = [url, {}, { server: `${config.spaceKey}`, transports: 'websocket' }] this.backoff = getBackoff({ delayMin: 50, - delayMax: 5000, + delayMax: 5000 }) this.maxAttempts = 7 + this.shouldClose = false FsSocketClient.$$singleton = this emitter.on(E.SOCKET_RETRY, this.reconnect.bind(this)) } @@ -35,16 +39,20 @@ class FsSocketClient { this.stompClient.debug = false // stop logging PING/PONG } const success = () => { - runInAction(() => config.fsSocketConnected = true) + runInAction(() => (config.fsSocketConnected = true)) this.backoff.reset() this.successCallback(this.stompClient) } const error = (frame) => { - log('fsSocket error', this.socket) + if (this.shouldClose) { + this.shouldClose = false + return + } + log('[FS Socket] FsSocket error', this.socket) switch (this.socket.readyState) { case SockJS.CLOSING: case SockJS.CLOSED: - runInAction(() => config.fsSocketConnected = false) + runInAction(() => (config.fsSocketConnected = false)) this.reconnect() break case SockJS.OPEN: @@ -55,23 +63,27 @@ class FsSocketClient { this.errorCallback(frame) } - this.stompClient.connect({}, success, error) + this.stompClient.connect( + {}, + success, + error + ) } reconnect () { if (config.fsSocketConnected) return - log(`try reconnect fsSocket ${this.backoff.attempts}`) + log(`[FS Socket] Try reconnect fsSocket ${this.backoff.attempts}`) // unset this.socket this.socket = undefined if (this.backoff.attempts <= this.maxAttempts) { const retryDelay = this.backoff.duration() log(`Retry after ${retryDelay}ms`) - const timer = setTimeout( - this.connect.bind(this) - , retryDelay) + const timer = setTimeout(this.connect.bind(this), retryDelay) } else { emitter.emit(E.SOCKET_TRIED_FAILED) - notify({ message: i18n`global.onSocketError`, notifyType: NOTIFY_TYPE.ERROR }) + notification.error({ + description: i18n`global.onSocketError` + }) this.backoff.reset() warn('Sock connected failed, something may be broken, reload page and try again') } @@ -80,37 +92,52 @@ class FsSocketClient { close () { const self = this if (config.fsSocketConnected) { - self.socket.close(1000, 123) - runInAction(() => config.fsSocketConnected = false) + self.shouldClose = true + self.socket.close() + emitter.emit(E.SOCKET_TRIED_FAILED) + runInAction(() => (config.fsSocketConnected = false)) + } + if (TtySocketClient.$$singleton) { + TtySocketClient.$$singleton.close() + } + if (SearchSocketClient.$$singleton) { + SearchSocketClient.$$singleton.close() } } } - class TtySocketClient { constructor () { if (TtySocketClient.$$singleton) return TtySocketClient.$$singleton if (config.isPlatform) { const wsUrl = config.wsURL const firstSlashIdx = wsUrl.indexOf('/', 8) - const [host, path] = firstSlashIdx === -1 ? [wsUrl, ''] : [wsUrl.substring(0, firstSlashIdx), wsUrl.substring(firstSlashIdx)] - this.socket = io.connect(host, { - forceNew: true, - reconnection: false, - autoConnect: false, // <- will manually handle all connect/reconnect behavior - reconnectionDelay: 1500, - reconnectionDelayMax: 10000, - reconnectionAttempts: 5, - path: `${path}/tty/${config.shardingGroup}/${config.spaceKey}/connect`, - transports: ['websocket'] - }) + const [host, path] = + firstSlashIdx === -1 + ? [wsUrl, ''] + : [wsUrl.substring(0, firstSlashIdx), wsUrl.substring(firstSlashIdx)] + this.socket = io.connect( + host, + { + forceNew: true, + reconnection: false, + autoConnect: false, // <- will manually handle all connect/reconnect behavior + reconnectionDelay: 1500, + reconnectionDelayMax: 10000, + reconnectionAttempts: 5, + path: `${path}/tty/${config.shardingGroup}/${config.spaceKey}/connect`, + transports: ['websocket'] + } + ) } else { - this.socket = io.connect(config.baseURL, { resource: 'coding-ide-tty1' }) + this.socket = io.connect( + config.baseURL, + { resource: 'coding-ide-tty1' } + ) } - this.backoff = getBackoff({ delayMin: 1500, - delayMax: 10000, + delayMax: 10000 }) this.maxAttempts = 5 @@ -126,10 +153,17 @@ class TtySocketClient { connect () { if (!config.isPlatform) return // Need to make sure EVERY ATTEMPT to connect has ensured `fsSocketConnected == true` - if (!this.socket || this.socket.connected || this.connectingPromise) return this.connectingPromise + if (!this.socket || this.socket.connected || this.connectingPromise) { + return this.connectingPromise + } let resolve, reject - this.connectingPromise = new Promise((rsv, rjt) => { resolve = rsv; reject = rjt }) - const dispose = autorun(() => { if (config.fsSocketConnected) resolve(true) }) + this.connectingPromise = new Promise((rsv, rjt) => { + resolve = rsv + reject = rjt + }) + const dispose = autorun(() => { + if (config.fsSocketConnected) resolve(true) + }) this.connectingPromise.then(() => { dispose() this.connectingPromise = undefined @@ -138,11 +172,11 @@ class TtySocketClient { // all logic above is just for ensuring `fsSocketConnected == true` this.socket.io.connect((err) => { if (err) { - runInAction(() => config.ttySocketConnected = false) + runInAction(() => (config.ttySocketConnected = false)) return this.reconnect() } // success! - runInAction(() => config.ttySocketConnected = true) + runInAction(() => (config.ttySocketConnected = true)) this.backoff.reset() }) this.socket.connect() @@ -150,22 +184,132 @@ class TtySocketClient { } reconnect () { - log(`try reconnect ttySocket ${this.backoff.attempts}`) if (this.backoff.attempts <= this.maxAttempts && !this.socket.connected) { const timer = setTimeout(() => { this.connect() + clearTimeout(timer) }, this.backoff.duration()) } else { - warn(`TTY reconnection fail after ${this.backoff.attempts} attempts`) + warn(`[TTY Socket] TTY reconnection fail after ${this.backoff.attempts} attempts`) this.backoff.reset() } } close () { if (config.ttySocketConnected) { this.socket.disconnect('manual') - TtySocketClient.$$singleton = null + // TtySocketClient.$$singleton = null + } + } +} + +class SearchSocketClient { + constructor () { + if (SearchSocketClient.$$singleton) return SearchSocketClient.$$singleton + + const wsUrl = config.wsURL + const firstSlashIdx = wsUrl.indexOf('/', 8) + const [host, path] = + firstSlashIdx === -1 + ? [wsUrl, ''] + : [wsUrl.substring(0, firstSlashIdx), wsUrl.substring(firstSlashIdx)] + + // const url = `${host}:8066/search/sockjs` + const url = `${host}${path}/search/sockjs/${config.spaceKey}` + // http://dev.coding.ide/ide-ws/search/sockjs/kfddvb/info + this.sockJSConfigs = [url, {}, { server: `${config.spaceKey}`, transports: 'websocket' }] + + this.backoff = getBackoff({ + delayMin: 1500, + delayMax: 10000 + }) + this.maxAttempts = 5 + + SearchSocketClient.$$singleton = this + emitter.on(E.SOCKET_RETRY, () => { + this.reconnect() + }) + } + + connect () { + if (!this.socket || !this.stompClient) { + this.socket = new SockJS(...this.sockJSConfigs) + this.stompClient = Stomp.over(this.socket) + this.stompClient.debug = false // stop logging PING/PONG + } + const success = () => { + runInAction(() => (config.searchSocketConnected = true)) + this.backoff.reset() + this.successCallback(this.stompClient) + state.searching.tip = '' + } + const error = (frame) => { + if (this.shouldClose) { + this.shouldClose = false + return + } + log('[SEARCH Socket] SearchSocket error', this.socket) + + state.searching.tip = i18n`panel.result.err` + + switch (this.socket.readyState) { + case SockJS.CLOSING: + case SockJS.CLOSED: + runInAction(() => (config.searchSocketConnected = false)) + this.reconnect() + break + case SockJS.OPEN: + log('FRAME ERROR', frame) + break + default: + } + this.errorCallback(frame) + } + + this.stompClient.connect( + {}, + success, + error + ) + } + + reconnect () { + if (config.searchSocketConnected) return + log(`[SEARCH Socket] reconnect searchSocket ${this.backoff.attempts}`) + // unset this.socket + this.socket = undefined + if (this.backoff.attempts <= this.maxAttempts) { + const retryDelay = this.backoff.duration() + log(`Retry after ${retryDelay}ms`) + const timer = setTimeout(this.connect.bind(this), retryDelay) + } else { + // must emit ,ops correct? + // emitter.emit(E.SOCKET_TRIED_FAILED) + notification.error({ + description: i18n`global.onSocketError` + }) + this.backoff.reset() + warn('Sock connected failed, something may be broken, reload page and try again') + } + } + + close () { + const self = this + if (config.searchSocketConnected) { + self.shouldClose = true + self.socket.close() + // must emit ??? + // emitter.emit(E.SOCKET_TRIED_FAILED); + runInAction(() => (config.searchSocketConnected = false)) } } + + subscribe = (topic, process) => { + this.stompClient.subscribe(topic, process) + } + + send = (mapping, headers, data) => { + this.stompClient.send(mapping, headers, data) + } } -export { FsSocketClient, TtySocketClient } +export { FsSocketClient, TtySocketClient, SearchSocketClient } diff --git a/app/backendAPI/workspaceAPI.js b/app/backendAPI/workspaceAPI.js index ea92ac80..5bb03cff 100644 --- a/app/backendAPI/workspaceAPI.js +++ b/app/backendAPI/workspaceAPI.js @@ -1,10 +1,13 @@ import config from '../config' import { request } from '../utils' -import { FsSocketClient, TtySocketClient } from './websocketClients' +import { FsSocketClient, TtySocketClient, SearchSocketClient } from './websocketClients' let connectedResolve export const fsSocketConnectedPromise = new Promise((rs, rj) => connectedResolve = rs) +let searchConnectedResolve; +export const searchSocketConnectedPromise = new Promise((rs, rj) => searchConnectedResolve = rs); + export function isWorkspaceExist () { return request.get(`/workspaces/${config.spaceKey}`) .catch(() => false) @@ -19,6 +22,16 @@ export function createWorkspace (options) { return request.post('/workspaces', options) } +export function findSpaceKey ({ ownerName, projectName }) { + return request.get(`/ws/find/coding/${ownerName}/${projectName}`, null, + { headers: { Accept: '*/*' } } + ).then(res => res.data) +} + +export function getWorkspace (spaceKey = config.spaceKey) { + return request.get(`/workspaces/${spaceKey}`) +} + export function connectWebsocketClient () { return new Promise((resolve) => { const fsSocketClient = new FsSocketClient() @@ -31,6 +44,18 @@ export function connectWebsocketClient () { }) } +export function connectSearchWebsocketClient() { + return new Promise((resolve) => { + const searchSocketClient = new SearchSocketClient(); + searchSocketClient.successCallback = function(stompClient) { + searchConnectedResolve(stompClient); + resolve(true); + } + searchSocketClient.errorCallback = function(error) {} + searchSocketClient.connect(); + }) +} + export function closeWebsocketClient () { const fsSocketClient = new FsSocketClient() fsSocketClient.close() @@ -41,6 +66,11 @@ export function closeTtySocketClient () { ttySocketClient.close() } +export function closeSearchWebsocketClient() { + const searchSocketClient = new SearchSocketClient(); + searchSocketClient.close(); +} + export function getSettings () { return request.get(`/workspaces/${config.spaceKey}/settings?base64=false`).then(({ content = {} }) => JSON.parse(content)) } @@ -60,3 +90,7 @@ export function execShellCommand (command) { export function getWorkspaceList () { return request.get('/workspaces?page=0&size=5&sort=lastModifiedDate,desc') } + +export function createProject (options) { + return request.post('/projects', options, { headers: { Accept: '*/*' } }) +} diff --git a/app/changelog.html b/app/changelog.html new file mode 100644 index 00000000..93c15def --- /dev/null +++ b/app/changelog.html @@ -0,0 +1,871 @@ + + + + + + Cloud Studio - 开启云端开发模式 WebIDE + + + + + + + + + + + + + + + + +
+
+
+ +
+ 注册 + | + 登录 +
+ +
+
+
+ + + + + +
+ +
+
+
+
+ +
+
+
+
+
+
+ +
+
+

Cloud Studio 更新日志

+

+ 在团队的努力下,Cloud Studio 持续进行着快速的产品迭代,为开发者提供最好的云端开发体验。如果您对产品有任何意见与建议,欢迎随时反馈。 +

+
+
+
+
+
联系我们
+
+
+
电话
+
400-930-9163
+
+
+
邮箱
+ support@coding.net +
+
+
在线反馈
+ https://feedback.coding.net/ +
+
+
腾讯云开发者平台由腾讯云及 CODING 共同运营,目前由 CODING 团队提供运营服务。
+
+
+
+ + + +
+
+ + + + + + + + diff --git a/app/commands/CommandPalette/component.jsx b/app/commands/CommandPalette/component.jsx index 838a3669..039c3219 100644 --- a/app/commands/CommandPalette/component.jsx +++ b/app/commands/CommandPalette/component.jsx @@ -17,7 +17,7 @@ class CommandPalette extends Component { return (
this.setState({items: getPaletteItems(e.target.value)}) } onKeyDown={this._onKeyDown} diff --git a/app/commands/commandBindings/editor.js b/app/commands/commandBindings/editor.js index 8460aae1..1a75ebe4 100644 --- a/app/commands/commandBindings/editor.js +++ b/app/commands/commandBindings/editor.js @@ -22,4 +22,3 @@ export default { Pane.split(4, 'row') } } - diff --git a/app/commands/commandBindings/file.js b/app/commands/commandBindings/file.js index 2543a271..887b8c0f 100644 --- a/app/commands/commandBindings/file.js +++ b/app/commands/commandBindings/file.js @@ -2,14 +2,19 @@ import mobxStore from '../../mobxStore' import { path as pathUtil } from '../../utils' import api from '../../backendAPI' import * as Modal from '../../components/Modal/actions' +import uniqueId from 'lodash/uniqueId' import TabStore from 'components/Tab/store' -import FileState from 'commons/File/state' +import TabState from 'components/Tab/state' +import FileState, { extState } from 'commons/File/state' import FileStore from 'commons/File/store' -import { notify } from '../../components/Notification/actions' +import notification from '../../components/Notification' import i18n from 'utils/createI18n' import icons from 'file-icons-js' +import config from 'config' import { toJS, when } from 'mobx' import emitter, { FILE_HIGHLIGHT } from 'utils/emitter' +import qs from 'qs' +import mime from 'mime-types' const nodeToNearestDirPath = (node) => { if (!node) node = { isDir: true, path: '/' } // fake a root node if !node @@ -24,48 +29,149 @@ const nodeToNearestDirPath = (node) => { const nodeToParentDirPath = (node) => { const pathSplitted = node.path.split('/') - if (pathSplitted.pop() == '') { pathSplitted.pop() } + if (pathSplitted.pop() == '') { + pathSplitted.pop() + } return `${pathSplitted.join('/')}/` } function createFolderAtPath (path) { - return api.createFolder(path) - .then((data) => { - if (data.code < 0) { - Modal.updateModal({ statusMessage: data.msg }).then(createFolderAtPath) - } else { - Modal.dismissModal() - } - }) - .then(() => path) - // if error, try again. - .catch(err => - Modal.updateModal({ statusMessage: err.msg }).then(createFolderAtPath) + return ( + api + .createFolder(path) + .then((data) => { + if (data.code < 0) { + Modal.updateModal({ statusMessage: data.msg }).then(createFolderAtPath) + } else { + Modal.dismissModal() + } + }) + .then(() => path) + // if error, try again. + .catch(err => Modal.updateModal({ statusMessage: err.msg }).then(createFolderAtPath)) ) } - -export function openFile (obj, callback) { - if (!obj.path) return - // 做一些encoding的调度 - if (FileState.initData.get('_init')) { - when(() => !FileState.initData.get('_init'), () => { - const { encoding } = FileState.initData.get(obj.path) || {} - openFileWithEncoding({ ...obj, encoding, callback }) - FileState.initData.set(obj.path, {}) +const getMIME = (path) => { + let contentType = mime.lookup(path) + if (contentType) { + const exts = ['php', 'javascript'] + exts.map((ext) => { + if (contentType.includes(ext)) { + contentType = contentType.replace('application', 'text') + } }) } else { - const { encoding } = FileState.initData.get(obj.path) || {} - openFileWithEncoding({ ...obj, encoding, callback }) - FileState.initData.set(obj.path, {}) + contentType = 'text/plain' } + + return contentType +} + +const openUrlFile = (files) => { + // open file depends on url + + let fileArr = [] + if (files) { + fileArr = files.split(',') + + const baseOpen = (i) => { + if (i < fileArr.length) { + let path = fileArr[i] + if (!path.startsWith('/')) { + path = `/${path}` + } + openFile( + { + path, + contentType: getMIME(path) + }, + () => { + i++ + baseOpen(i) + } + ) + } + } + + baseOpen(0) + } +} + +export function initOpenFile (tabs, tabGroups) { + when( + () => !FileState.initData.get('_init'), + () => { + const openedTabs = Object.values(TabState.tabs._data) + tabs = tabs.filter((tab) => { + let flag = true + for (let i = 0, n = openedTabs.length; i < n; i++) { + const cc = openedTabs[i] + const path = cc.value.file ? cc.value.file.path : '' + if (tab.path === path) { + flag = false + } + } + if (flag) { + return tab + } + }) + + tabs.map((tabValue) => { + const { path, editor, contentType, ...others } = tabValue + FileStore.loadNodeData({ ...tabValue, isEditorLoading: true }) + TabStore.createTab({ + icon: icons.getClassWithColor(path.split('/').pop()) || 'fa fa-file-text-o', + contentType, + editor: { + ...editor, + filePath: path + }, + ...others + }) + const { encoding } = FileState.initData.get(path) || {} + api.readFile(path, encoding).then((data) => { + FileStore.loadNodeData({ ...data, isEditorLoading: false }) + FileState.initData.set(path, {}) + }) + }) + + const files = qs.parse(window.location.search.slice(1)).open + if (files) { + openUrlFile(files) + return + } + + tabGroups.forEach((tabGroupsValue) => { + const activeTabId = tabGroupsValue.activeTabId + if (activeTabId) { + const index = tabs.findIndex(tab => tab.id === activeTabId) + if (index >= 0) { + const { path, contentType } = tabs[index] + openFile({ + path, + contentType + }) + } + } + }) + } + ) } -export function openFileWithEncoding ({ path, editor = {}, others = {}, allGroup = false, encoding, callback }) { +export function openFile (obj, callback) { + const { path, contentType, editor = {}, others = {}, allGroup = false } = obj + if (!path) return + const { encoding } = FileState.initData.get(path) || {} const { encoding: currentEncoding } = FileStore.get(path) || {} - return api.readFile(path, encoding || currentEncoding) + return api + .readFile(path, encoding || currentEncoding) .then((data) => { FileStore.loadNodeData(data) + FileState.initData.set(path, {}) + for (const listener of extState.didOpenListeners) { + listener(FileState.entities.get(path)) + } return data }) .then(() => { @@ -78,35 +184,59 @@ export function openFileWithEncoding ({ path, editor = {}, others = {}, allGroup if (editor.gitBlame) { existingTab.editor.gitBlame = editor.gitBlame } + if (editor.selection) { + const { startLineNumber, startColumn } = editor.selection + + const pos = { + lineNumber: startLineNumber, + column: startColumn + } + + setTimeout(() => { + existingTab.editorInfo.monacoEditor.setSelection(editor.selection) + existingTab.editorInfo.monacoEditor.revealPositionInCenter(pos, 1) + existingTab.editorInfo.monacoEditor.focus() + }, 0) + } + + if (editor.debug) { + existingTab.editorInfo.debug = true + existingTab.editorInfo.line = editor.line + existingTab.editorInfo.stoppedReason = editor.stoppedReason + existingTab.editorInfo.setDebugDeltaDecorations() + } existingTab.activate() - if (callback) callback() + if (callback) callback(existingTab.id) } else { + const tabId = uniqueId('tab_') TabStore.createTab({ icon: icons.getClassWithColor(path.split('/').pop()) || 'fa fa-file-text-o', + contentType, editor: { ...editor, - filePath: path, + filePath: path }, + id: tabId, ...others }) if (callback) { - callback() + callback(tabId) } } }) -} - -function createTab ({ icon, type }) { - TabStore.createTab({ - icon, - type, - }) + .catch((e) => { + if (callback) { + callback(null) + } + }) } function createFileWithContent (content) { return function createFileAtPath (path) { + path = path.startsWith('/') ? path : `/${path}` if (content) { - return api.createFile(path, content) + return api + .createFile(path, content) .then((res) => { if (res.msg) { throw new Error(res.msg) @@ -123,41 +253,51 @@ function createFileWithContent (content) { TabStore.updateTab({ icon: (path && icons.getClassWithColor(path.split('/').pop())) || 'fa fa-file-text-o', id: activeTab.id, - editor: { filePath: path }, + editor: { filePath: path } }) }) }) .catch((res) => { - Modal.updateModal({ statusMessage: res.response ? res.response.data.msg : res.message }).then(createFileAtPath) + Modal.updateModal({ + statusMessage: res.response ? res.response.data.msg : res.message + }).then(createFileAtPath) }) } - return api.createFile(path, content) - .then((res) => { - if (res.msg) { - throw new Error(res.msg) - } else { - Modal.dismissModal() - } - }) - .then(() => { - openFile({ path }) - }) - // if error, try again. - .catch((res) => { - Modal.updateModal({ statusMessage: res.response ? res.response.data.msg : res.message }).then(createFileAtPath) - }) + return ( + api + .createFile(path, content) + .then((res) => { + if (res.msg) { + throw new Error(res.msg) + } else { + Modal.dismissModal() + return res + } + }) + .then((res) => { + openFile({ path, contentType: res.contentType }) + }) + // if error, try again. + .catch((res) => { + Modal.updateModal({ + statusMessage: res.response ? res.response.data.msg : res.message + }).then(createFileAtPath) + }) + ) } } const fileCommands = { - 'file:open_file': (c) => { // 在当前 tabgroup 中优先打开已有的 tab + 'file:open_file': (c) => { + // 在当前 tabgroup 中优先打开已有的 tab if (typeof c.data === 'string') { openFile({ path: c.data }) } else { openFile(c.data) } }, - 'file:open_exist_file': (c) => { // 在所有 tabgroup 中优先打开已有的 tab + 'file:open_exist_file': (c) => { + // 在所有 tabgroup 中优先打开已有的 tab if (typeof c.data === 'string') { openFile({ path: c.data, allGroup: true }) } else { @@ -165,7 +305,6 @@ const fileCommands = { } }, 'file:highlight_line': (c) => { - console.log('file:highlight_line', c) const { path, lineNumber } = c.data openFile({ path, allGroup: true }, () => { emitter.emit(FILE_HIGHLIGHT, c.data) @@ -182,8 +321,7 @@ const fileCommands = { message: i18n`file.newFilePath`, defaultValue, selectionRange: [path.length, defaultValue.length] - }) - .then(createFile) + }).then(createFile) }, 'file:new_folder': (c) => { const node = c.context @@ -192,14 +330,13 @@ const fileCommands = { Modal.showModal('Prompt', { message: i18n`file.newFileFolderPath`, defaultValue, - selectionRange: [path.length, defaultValue.length], + selectionRange: [path.length, defaultValue.length] }).then(createFolderAtPath) }, 'file:save': (c) => { const { EditorTabState } = mobxStore const activeTab = EditorTabState.activeTab - const content = activeTab ? activeTab.editor.cm.getValue() : '' - + const content = activeTab.editorInfo.monacoEditor.getValue() if (!activeTab.file) { const createFile = createFileWithContent(content) const defaultPath = activeTab._title ? `/${activeTab._title}` : '/untitled' @@ -207,38 +344,50 @@ const fileCommands = { message: i18n`file.newFilePath`, defaultValue: defaultPath, selectionRange: [1, defaultPath.length] - }) - .then(createFile) + }).then(createFile) } else { - api.writeFile(activeTab.file.path, content) - .then((res) => { - FileStore.updateFile({ - path: activeTab.file.path, - isSynced: true, - lastModified: res.lastModified, - // content, - }) + api.writeFile(activeTab.file.path, content).then((res) => { + FileStore.updateFile({ + path: activeTab.file.path, + isSynced: true, + lastModified: res.lastModified + // content, }) + }) } }, - + 'file:save_monaco': (context) => { + const { data } = context + const { EditorTabState } = mobxStore + const activeTab = EditorTabState.activeTab + if (activeTab.file) { + api.writeFile(activeTab.file.path, data).then((res) => { + FileStore.updateFile({ + path: activeTab.file.path, + isSynced: true, + lastModified: res.lastModified + // content, + }) + }) + } + }, 'file:rename': (c) => { const node = c.context const oldPath = node.path const parentPath = nodeToParentDirPath(node) - const existingTabs = TabStore.findTab( - tab => tab.file && tab.file.path.startsWith(oldPath) - ) + const existingTabs = TabStore.findTab(tab => tab.file && tab.file.path.startsWith(oldPath)) const moveFile = (from, to, force) => { - api.moveFile(from, to, force) + api + .moveFile(from, to, force) .then(() => Modal.dismissModal()) .catch(err => Modal.updateModal({ statusMessage: err.msg }).then((_to, _force) => moveFile(from, _to, _force) ) - ).then(() => { + ) + .then(() => { if (existingTabs.length) { existingTabs.forEach((tab) => { const newPath = tab.file.path.replace(from, to) @@ -258,7 +407,6 @@ const fileCommands = { }).then(newPath => moveFile(oldPath, newPath)) }, - 'file:delete': async (c) => { const confirmed = await Modal.showModal('Confirm', { header: i18n`file.deleteHeader`, @@ -267,10 +415,17 @@ const fileCommands = { }) if (confirmed) { - api.deleteFile(c.context.path) - .then(() => notify({ message: i18n`file.deleteNotifySuccess` })) + api + .deleteFile(c.context.path) + .then(() => + notification.success({ + description: i18n`file.deleteNotifySuccess` + }) + ) .catch(err => - notify({ message: i18n`file.deleteNotifyFailed${err.msg}` }) + notification.error({ + description: i18n`file.deleteNotifyFailed${err.msg}` + }) ) } @@ -284,20 +439,93 @@ const fileCommands = { // 'file:unsaved_files_list': 'file:open_welcome': (c) => { // const activeTabGroup = TabStore.getState().activeTabGroup - const existingTabs = TabStore.findTab( - tab => tab.type === 'welcome' - ) + const existingTabs = TabStore.findTab(tab => tab.type === 'welcome') if (existingTabs.length) { const existingTab = existingTabs[0] existingTab.activate() } else { TabStore.createTab({ - icon: 'fa fa-info-circle', + icon: 'fa fa-smile-o', type: 'welcome', - title: 'Welcome', + title: 'Welcome' + }) + } + }, + 'file:open_changelog': () => { + const existingTabs = TabStore.findTab(tab => tab.type === 'changelog') + if (existingTabs.length) { + const existingTab = existingTabs[0] + existingTab.activate() + } else { + TabStore.createTab({ + type: 'changelog', + title: 'Changelog' }) } }, + 'file:open_about': () => { + Modal.showModal({ type: 'About', position: 'center' }) + }, + + 'file:add_ignore': (c) => { + const ignorePath = '/.gitignore' + const ignoreExists = FileStore.get(ignorePath) + + const { isDir, path } = c.context + const patchTxt = isDir ? `${path}/` : path + + if (ignoreExists) { + api.readFile(ignorePath).then((ignoreFile) => { + const ignoreContent = ignoreFile.content + !ignoreContent.split('\n').some(item => patchTxt.startsWith(item)) + ? api + .writeFile( + ignorePath, + ignoreFile.content ? `${ignoreFile.content}\n${patchTxt}` : patchTxt + ) + .then((res) => { + FileStore.updateFile({ + path: ignorePath, + isSynced: true, + lastModified: res.lastModified + }) + notification.success({ + description: i18n`file.updateIgnoreSuccess` + }) + openFile({ path: ignorePath }) + }) + .catch((err) => { + notification.error({ + description: i18n`file.updateIgnoreFailed${{ err: err.msg }}` + }) + }) + : notification.error({ + description: i18n`file.updateIgnoreTip${{ path }}` + }) + }) + } else { + api + .createFile(ignorePath, patchTxt) + .then((res) => { + if (res.msg) { + throw new Error(res.msg) + } + }) + .then(() => api.writeFile(ignorePath, patchTxt)) + .then(() => { + notification.success({ + description: i18n`file.updateIgnoreSuccess` + }) + openFile({ path: ignorePath }) + }) + .catch((err) => { + notification.error({ + description: i18n`file.updateIgnoreFailed${{ err: err.msg }}` + }) + + }) + } + } } export default fileCommands diff --git a/app/commands/commandBindings/git.js b/app/commands/commandBindings/git.js index d201c0a7..889076b4 100644 --- a/app/commands/commandBindings/git.js +++ b/app/commands/commandBindings/git.js @@ -2,84 +2,82 @@ import store, { dispatch as $d } from '../../store' import api from '../../backendAPI' import * as Git from '../../components/Git/actions' import * as Modal from '../../components/Modal/actions' +import notification from 'components/Notification' +import config from 'config' export default { + 'git:remote': () => { + Modal.showModal({ type: 'ResetRemote' }) + }, + 'git:initialize': () => { + api.gitInit().then((data) => { + notification.success({ + description: data.msg + }) + }) + }, 'git:commit': (c) => { - api.gitStatus().then(({ files, clean }) => { - $d(Git.updateStatus({ files, isClean: clean })) - }).then(() => - Modal.showModal('GitCommit', 'HelloYo') - ) + api + .gitStatus() + .then(({ files, clean }) => { + $d(Git.updateStatus({ files, isClean: clean })) + }) + .then(() => { + Modal.showModal('GitCommitView') + }) }, 'git:pull': c => $d(Git.pull()), 'git:push': c => $d(Git.push()), - 'git:delete_branch': c => $d(Git.gitDeleteBranch(c).then( - () => { $d(Git.getBranches()) } - )), + 'git:delete_branch': c => + $d( + Git.gitDeleteBranch(c).then(() => { + $d(Git.getBranches()) + }) + ), 'git:resolve_conflicts': (c) => { - api.gitStatus().then(({ files, clean }) => { - files = _.filter(files, file => file.status == 'CONFLICTION') - $d(Git.updateStatus({ files, isClean: clean })) - }).then(() => - Modal.showModal('GitResolveConflicts') - ) + api + .gitStatus() + .then(({ files, clean }) => { + files = _.filter(files, file => file.status == 'CONFLICTION') + $d(Git.updateStatus({ files, isClean: clean })) + }) + .then(() => Modal.showModal('GitResolveConflicts')) }, // 'git:commit_and_push': 'git:checkout_new_branch': (c) => { $d(Git.getBranches()).then(() => - $d(Git.getCurrentBranch()).then(() => - Modal.showModal('GitCheckout', c.data) - ) + $d(Git.getCurrentBranch()).then(() => Modal.showModal('GitCheckout', c.data)) ) }, 'git:new_branch': (c) => { $d(Git.getBranches()).then(() => - $d(Git.getCurrentBranch()).then(() => - Modal.showModal('GitNewBranch') - ) + $d(Git.getCurrentBranch()).then(() => Modal.showModal('GitNewBranch')) ) }, 'git:tag': (c) => { - $d(Git.getCurrentBranch()).then(() => - $d(Git.getTags()) - .then(() => - Modal.showModal('GitTag') - ) - ) + $d(Git.getCurrentBranch()).then(() => $d(Git.getTags()).then(() => Modal.showModal('GitTag'))) }, 'git:merge': (c) => { $d(Git.getBranches()).then(() => - $d(Git.getCurrentBranch()).then(() => - Modal.showModal('GitMerge') - ) + $d(Git.getCurrentBranch()).then(() => Modal.showModal('GitMerge')) ) }, 'git:stash': (c) => { - $d(Git.getCurrentBranch()).then(() => - Modal.showModal('GitStash') - ) + $d(Git.getCurrentBranch()).then(() => Modal.showModal('GitStash')) }, 'git:unstash': (c) => { $d(Git.getCurrentBranch()).then(() => { - $d(Git.getStashList()) - .then(() => - Modal.showModal('GitUnstash') - ) + $d(Git.getStashList()).then(() => Modal.showModal('GitUnstash')) }) }, 'git:reset_head': (c) => { - $d(Git.getCurrentBranch()).then(() => - Modal.showModal('GitResetHead') - ) + $d(Git.getCurrentBranch()).then(() => Modal.showModal('GitResetHead')) }, 'git:rebase:start': (c) => { $d(Git.getBranches()).then(() => { - $d(Git.getTags()) - .then(() => - Modal.showModal('GitRebaseStart') - ) + $d(Git.getTags()).then(() => Modal.showModal('GitRebaseStart')) }) }, 'git:rebase:abort': (c) => { @@ -93,32 +91,40 @@ export default { }, 'git:history:compare': (c) => { const focusedNode = c.context.focusedNode - $d(Git.diffFile({ - path: focusedNode.path, - newRef: c.context.shortName, - oldRef: `${c.context.shortName}^` - })) + $d( + Git.diffFile({ + path: focusedNode.path, + newRef: c.context.shortName, + oldRef: `${c.context.shortName}^` + }) + ) }, 'git:history:compare_local': (c) => { const focusedNode = c.context.focusedNode if (!focusedNode || focusedNode.isDir) { - $d(Git.gitCommitDiff({ - rev: c.context.shortName, - title: 'Show Commit' - })) + $d( + Git.gitCommitDiff({ + rev: c.context.shortName, + title: 'Show Commit' + }) + ) } else { - $d(Git.diffFile({ - path: focusedNode.path, - newRef: c.context.shortName, - oldRef: '~~unstaged~~' - })) + $d( + Git.diffFile({ + path: focusedNode.path, + newRef: c.context.shortName, + oldRef: '~~unstaged~~' + }) + ) } }, 'git:history:all_effected': (c) => { - $d(Git.gitCommitDiff({ - rev: c.context.shortName, - title: 'Show Commit', - oldRef: `${c.context.shortName}^` - })) + $d( + Git.gitCommitDiff({ + rev: c.context.shortName, + title: 'Show Commit', + oldRef: `${c.context.shortName}^` + }) + ) } } diff --git a/app/commands/commandBindings/misc.js b/app/commands/commandBindings/misc.js index db32cb88..009200e3 100644 --- a/app/commands/commandBindings/misc.js +++ b/app/commands/commandBindings/misc.js @@ -3,6 +3,7 @@ import * as Panel from 'components/Panel/actions' import * as SideBar from 'components/Panel/SideBar/actions' import terminalState from 'components/Terminal/state' import * as Terminal from 'components/Terminal/actions' +import * as searchState from 'commons/Search/state' const getComponentByName = name => window.refs[name].getWrappedInstance() export default { @@ -20,6 +21,12 @@ export default { 'global:show_branches': () => { getComponentByName('GitBranchWidget').toggleActive(true) }, + 'global:show_env': () => { + SideBar.toggleSidePanelView('SIDEBAR.RIGHT.env') + }, + 'global:show_search': () => { + SideBar.toggleSidePanelView('SIDEBAR.LEFT.find') + }, 'modal:dismiss': (c) => { Modal.dismissModal() }, diff --git a/app/commands/commandBindings/tab.js b/app/commands/commandBindings/tab.js index 1959bb74..34801d9f 100644 --- a/app/commands/commandBindings/tab.js +++ b/app/commands/commandBindings/tab.js @@ -5,15 +5,15 @@ import * as PaneActions from 'components/Pane/actions' export default { 'tab:close': (c) => { - Tab.removeTab(c.context.id) + Tab.removeTab(c.context) }, 'tab:close_other': (c) => { - Tab.removeOtherTab(c.context.id) + Tab.removeOtherTab(c.context) }, 'tab:close_all': (c) => { - Tab.removeAllTab(c.context.id) + Tab.removeAllTab(c.context) }, 'tab:split_v': (c) => { @@ -35,4 +35,36 @@ export default { Tab.moveTabToPane(c.context.id, newPaneId) ) }, + + 'tab:zenmode': () => { + const tab = document.querySelector('.tab-content-item.active'); + const datasetExt = tab.dataset.ext; + const ext = datasetExt && datasetExt.toLowerCase(); + if (['md', 'markdown', 'html'].includes(ext)) { + tab.classList.add('zenmode_preview'); + } else { + tab.classList.add('zenmode'); + } + if (tab.requestFullscreen) { + tab.requestFullscreen(); + } else if (tab.webkitRequestFullscreen) { + tab.webkitRequestFullscreen(); + } else if (tab.mozRequestFullScreen) { + tab.mozRequestFullScreen(); + } else if (tab.msRequestFullscreen) { + tab.msRequestFullscreen(); + } + } +} + +function exitFullScreen() { + const isFull = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement; + if (!isFull) { + const tab = document.querySelector('.tab-content-item.active'); + tab.classList.remove('zenmode', 'zenmode_preview'); + } } +document.addEventListener('fullscreenchange', () => exitFullScreen()); +document.addEventListener('webkitfullscreenchange', () => exitFullScreen()); +document.addEventListener('mozfullscreenchange', () => exitFullScreen()); +document.addEventListener('msfullscreenchang', () => exitFullScreen()); diff --git a/app/commands/index.js b/app/commands/index.js index 5c9ca0a3..35fcb845 100644 --- a/app/commands/index.js +++ b/app/commands/index.js @@ -1,12 +1,17 @@ import { emitter } from 'utils' import Keymapper from './lib/keymapper' -import keymaps from './keymaps' +import { systemKeymaps, pluginsKeymaps } from './keymaps' import commandBindings from './commandBindings' +import { flattenKeyMaps } from './lib/helpers' import dispatchCommand, { setContext, addCommand } from './dispatchCommand' import { CommandPalette } from './CommandPalette' +// import { pluinKeymapsForPlatform } from './pluginsKeymaps' +export const key = new Keymapper({ dispatchCommand }) +key.loadKeymaps(systemKeymaps) -const key = new Keymapper({ dispatchCommand }) -key.loadKeymaps(keymaps) +pluginsKeymaps.forEach((keyConfig) => { + key.loadKeymaps(flattenKeyMaps(keyConfig.keymaps)) +}) Object.keys(commandBindings).map((commandType) => { emitter.on(commandType, commandBindings[commandType]) diff --git a/app/commands/keymaps.js b/app/commands/keymaps.js index a409d5e1..5ee926d4 100644 --- a/app/commands/keymaps.js +++ b/app/commands/keymaps.js @@ -1,67 +1,123 @@ +import i18n from 'utils/createI18n' +import { observable } from 'mobx' +import { flattenKeyMaps } from './lib/helpers' + // Unavailable shortcuts: shift / ctrl + (q|n|w|t|↹) const os = (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase() -export const isMac = (os === 'mac') +export const isMac = os === 'mac' -let keymaps -let modifierKeysMap -if (isMac) { - keymaps = { - 'alt+n': 'file:new_file', - 'alt+shift+n': 'file:new_folder', - 'cmd+s': 'file:save', - 'cmd+ctrl+c': 'git:commit', - 'esc': 'modal:dismiss', - 'cmd+shift+p': 'global:command_palette', - 'cmd+p': 'global:file_palette', - 'cmd+alt+1': 'editor:split_pane_1', - 'cmd+alt+shift+1': 'editor:split_pane_1', - 'cmd+alt+2': 'editor:split_pane_vertical_2', - 'cmd+alt+shift+2': 'editor:split_pane_horizontal_2', - 'cmd+alt+3': 'editor:split_pane_vertical_3', - 'cmd+alt+shift+3': 'editor:split_pane_horizontal_3', - 'cmd+alt+4': 'editor:split_pane_vertical_4', - 'cmd+,': 'global:show_settings', - 'ctrl+g': 'editor:goto', - 'alt+l': 'edit:toggle_format', - 'cmd+/': 'edit:toggle_comment', - } - modifierKeysMap = { +export const modifierKeysMap = isMac + ? { ctrl: '⌃', alt: '⌥', cmd: '⌘', - shift: '⇧', + shift: '⇧' } -} else { - keymaps = { - 'alt+n': 'file:new_file', - 'alt+shift+n': 'file:new_folder', - 'ctrl+s': 'file:save', - 'ctrl+alt+c': 'git:commit', - 'esc': 'modal:dismiss', - 'ctrl+shift+p': 'global:command_palette', - 'ctrl+p': 'global:file_palette', - 'ctrl+alt+1': 'editor:split_pane_1', - 'ctrl+alt+shift+1': 'editor:split_pane_1', - 'ctrl+alt+2': 'editor:split_pane_vertical_2', - 'ctrl+alt+shift+2': 'editor:split_pane_horizontal_2', - 'ctrl+alt+3': 'editor:split_pane_vertical_3', - 'ctrl+alt+shift+3': 'editor:split_pane_horizontal_3', - 'ctrl+alt+4': 'editor:split_pane_vertical_4', - 'alt+,': 'global:show_settings', - 'ctrl+g': 'editor:goto', - 'alt+l': 'edit:toggle_format', - 'ctrl+/': 'edit:toggle_comment', - } - modifierKeysMap = { + : { ctrl: 'Ctrl', alt: 'Alt', cmd: 'Cmd', - shift: 'Shift', + shift: 'Shift' } -} -export default keymaps +const keymapStore = observable({ + systemKeymaps: [ + { + command: 'file:new_file', + mac: 'alt+n', + win: 'alt+n', + label: i18n`settings.keymap.createFile` + }, + { + command: 'file:new_folder', + mac: 'alt+shift+n', + win: 'alt+shift+n', + label: i18n`settings.keymap.createFolder` + }, + { + command: 'file:save', + mac: 'cmd+s', + win: 'ctrl+s', + label: i18n`settings.keymap.saveFile` + }, + { + command: 'modal:dismiss', + mac: 'esc', + win: 'esc', + label: i18n`settings.keymap.exitModal` + }, + { + command: 'git:commit', + mac: 'cmd+ctrl+c', + win: 'ctrl+alt+c', + label: i18n`settings.keymap.gitCommit` + }, + { + command: 'global:command_palette', + mac: 'cmd+shift+p', + win: 'ctrl+shift+p', + label: i18n`settings.keymap.commandPalette` + }, + { + command: 'global:file_palette', + mac: 'cmd+p', + win: 'ctrl+p', + label: i18n`settings.keymap.filePalette` + }, + { + command: 'global:show_settings', + mac: 'cmd+,', + win: 'alt+,', + label: i18n`settings.keymap.showSettings` + }, + { + command: 'editor:goto', + mac: 'cmd+g', + win: 'ctrl+g', + label: i18n`file.goto` + }, + { + command: 'edit:toggle_format_monaco', + mac: 'alt+l', + win: 'alt+l', + label: i18n`settings.keymap.toggleFormat` + }, + { + command: 'edit:toggle_monaco_comment', + mac: 'cmd+/', + win: 'ctrl+/', + label: i18n`settings.keymap.toggleComment` + }, + { + command: 'tab:zenmode', + mac: 'cmd+f11', + win: 'ctrl+f11', + label: i18n`settings.keymap.into_zenmode` + }, + { + command: 'global:show_search', + mac: 'cmd+shift+f', + win: 'ctrl+shift+f', + label: i18n`settings.keymap.search` + } + ], + pluginsKeymaps: [], +}) -export { - modifierKeysMap +export default keymapStore + +export const systemKeymaps = flattenKeyMaps(keymapStore.systemKeymaps) + +export const pluginsKeymaps = keymapStore.pluginsKeymaps + +export function getFlattenAllKeymaps () { + return { + ...flattenKeyMaps(keymapStore.systemKeymaps), + ...flattenKeyMaps( + keymapStore.pluginsKeymaps.map(km => km.keymaps).reduce((pre, cur) => { + pre = [...pre, ...cur] + return pre + }, []) + ) + } } diff --git a/app/commands/lib/helpers.js b/app/commands/lib/helpers.js index 60f2dfd3..b90821a9 100644 --- a/app/commands/lib/helpers.js +++ b/app/commands/lib/helpers.js @@ -1,6 +1,9 @@ const keycodes = require('./keycodes') const MODIFIERS_LIST = ['meta', 'ctrl', 'shift', 'alt'] +const os = (navigator.platform.match(/mac|win|linux/i) || ['other'])[0].toLowerCase() +export const isMac = (os === 'mac') + export function keyEventToKeyCombination (e, combinator) { // ensure comb always in the order spec by MODIFIERS_LIST const modString = MODIFIERS_LIST.filter(mod => e[`${mod}Key`]).join(combinator) @@ -30,3 +33,15 @@ export function normalizeKeys (keys, combinator='+', delimiter=' ') { }) .join(delimiter) } + +export function flattenKeyMaps (keymaps) { + return keymaps.reduce((pre, cur) => { + const { mac, win, command } = cur + if (isMac) { + pre[mac] = command + } else { + pre[win] = command + } + return pre + }, {}) +} diff --git a/app/commands/lib/keymapper.js b/app/commands/lib/keymapper.js index 54967971..5fea6bba 100644 --- a/app/commands/lib/keymapper.js +++ b/app/commands/lib/keymapper.js @@ -6,14 +6,27 @@ Mousetrap.prototype.stopCallback = function () { return false } class Keymapper { constructor ({ dispatchCommand }) { this.dispatchCommand = dispatchCommand + this.keyStore = [] } loadKeymaps (keymaps) { Object.entries(keymaps).map(([keycombo, commandType]) => { - Mousetrap.bind(normalizeKeys(keycombo), (e) => { - this.dispatchCommand(commandType) - e.preventDefault(); e.stopPropagation() - }) + if (!this.keyStore.includes(keycombo)) { + this.keyStore.push(keycombo) + Mousetrap.bind(normalizeKeys(keycombo), (e) => { + this.dispatchCommand(commandType) + e.preventDefault(); e.stopPropagation() + }) + } + }) + } + + unbindKeymaps (keymaps) { + Object.entries(keymaps).map(([keycombo]) => { + if (this.keyStore.includes(keycombo)) { + this.keyStore = this.keyStore.filter((key) => key !== keycombo) + } + Mousetrap.unbind(normalizeKeys(keycombo)) }) } } diff --git a/app/commands/pluginsKeymaps.js b/app/commands/pluginsKeymaps.js new file mode 100644 index 00000000..483aa234 --- /dev/null +++ b/app/commands/pluginsKeymaps.js @@ -0,0 +1,22 @@ +import { flatten } from 'lodash' +import { flattenKeyMaps } from './lib/helpers' +/** + * ```typescript + * interface IKeyMaps { + * command: string; + * mac: string; + * win: string; + * } + * interface IPluginKeyMaps { + * name: string; + * keymaps: Array; + * } + * ``` + */ + +let pluginsKeymaps = [] + +const allKeymaps = flatten(pluginsKeymaps.map((km) => km.keymaps)) +export const pluinKeymapsForPlatform = flattenKeyMaps(allKeymaps) + +export default pluginsKeymaps diff --git a/app/commons/File/actions.js b/app/commons/File/actions.js index 93abe82c..e3052e11 100644 --- a/app/commons/File/actions.js +++ b/app/commons/File/actions.js @@ -1,41 +1,107 @@ import flattenDeep from 'lodash/flattenDeep' import { registerAction } from 'utils/actions' +import settings from 'settings' import is from 'utils/is' -import { action, when } from 'mobx' import api from 'backendAPI' +import config from 'config' +import { showModal } from 'components/Modal/actions' +import { fetchLanguageServerSetting } from 'backendAPI/languageServerAPI' +import { findLanguagesByFileList } from 'components/MonacoEditor/utils/findLanguage' import state, { FileNode } from './state' export function fetchPath (path) { - return api.fetchPath(path).then(nodePropsList => Promise.all(nodePropsList.map((nodeProps) => { - if (nodeProps.isDir && nodeProps.directoriesCount === 1 && nodeProps.filesCount === 0) { - return fetchPath(nodeProps.path).then(data => [nodeProps].concat(data)) + return api.fetchPath(path).then(nodePropsList => + Promise.all( + nodePropsList.map((nodeProps) => { + if (nodeProps.isDir && nodeProps.directoriesCount === 1 && nodeProps.filesCount === 0) { + return fetchPath(nodeProps.path).then(data => [nodeProps].concat(data)) + } + return Promise.resolve(nodeProps) + }) + ).then(flattenDeep) + ) +} + +export const loadNodeData = registerAction('fs:load_node_data', (nodePropsList) => { + if (!is.array(nodePropsList)) nodePropsList = [nodePropsList] + return nodePropsList.map((nodeProps) => { + const curNode = state.entities.get(nodeProps.path) + if (curNode) { + const curNodeTime = new Date(curNode.lastModified) + const newNodeTime = new Date(nodeProps.lastModified) + if (newNodeTime.getTime() > curNodeTime.getTime() || !curNodeTime.content) { + curNode.update(nodeProps) + } + return curNode + } + const newNode = new FileNode(nodeProps) + state.entities.set(newNode.path, newNode) + return newNode + }) +}) + +function tryIdentificationWorkSpaceType (files) { + return new Promise(async (resolve, _) => { + const types = findLanguagesByFileList(files) + .map(type => ({ srcPath: '/', type })) + if (!types || types.length === 0) { + Promise.all( + files.filter(file => file.isDir) + .map(async (task) => { + const result = await api.fetchPath(task.path) + const subTypes = findLanguagesByFileList(result) + .map(type => ({ srcPath: task.path, type })) + return Promise.resolve(subTypes) + }) + ) + .then((langSettings) => { + resolve(flattenDeep(langSettings.filter(type => type.length > 0))) + }) + } else { + resolve(types) } - return Promise.resolve(nodeProps) - })).then(flattenDeep)) + }) } -export const loadNodeData = registerAction('fs:load_node_data', - (nodePropsList) => { - if (!is.array(nodePropsList)) nodePropsList = [nodePropsList] - return nodePropsList.map((nodeProps) => { - const curNode = state.entities.get(nodeProps.path) - if (curNode) { - const curNodeTime = new Date(curNode.lastModified) - const newNodeTime = new Date(nodeProps.lastModified) - if (newNodeTime.getTime() > curNodeTime.getTime() || !curNodeTime.content) { - curNode.update(nodeProps) - } - return curNode +const setLanguageSetting = (data) => { + if (data.length > 1) { + showModal({ type: 'ProjectTypeSelector', position: 'center', data }) + } else if (data.length === 1) { + const { type, srcPath } = data[0] + config.mainLanguage = type + settings.languageserver.projectType.value = type + settings.languageserver.sourcePath.value = srcPath + } +} + +const fetchPackageJsonFile = () => { + api.readFile('/package.json') + .then(res => { + const content = JSON.parse(res.content) + if (content && content.codingIdePackage && content.codingIdePackage.type === 'plugin') { + config.__PLUGIN_DEV__ = true } - const newNode = new FileNode(nodeProps) - state.entities.set(newNode.path, newNode) - return newNode }) - } -) +} export const fetchProjectRoot = registerAction('fs:init', () => - fetchPath('/').then(loadNodeData) + fetchPath('/').then((data) => { + /** + * 插件工作空间判断 + */ + if (data.find(file => file.path === '/package.json')) { + fetchPackageJsonFile() + } + fetchLanguageServerSetting(config.spaceKey).then((res) => { + if (res.code === 0 && res.data) { + setLanguageSetting([res.data]) + } else { + tryIdentificationWorkSpaceType(data) + .then(setLanguageSetting) + } + }) + return loadNodeData(data) + }) ) export const removeNode = registerAction('fs:remove_node', (node) => { @@ -66,5 +132,10 @@ export const syncFile = registerAction('fs:sync', (params) => { encoding = params.encoding } const fileNode = state.entities.get(path) - if (!fileNode.isDir) return api.readFile(path, encoding).then(loadNodeData).then(files => files[0]) + if (!!fileNode && !fileNode.isDir) { + return api + .readFile(path, encoding) + .then(loadNodeData) + .then(files => files[0]) + } }) diff --git a/app/commons/File/state.js b/app/commons/File/state.js index f5817a59..c6d1fe02 100644 --- a/app/commons/File/state.js +++ b/app/commons/File/state.js @@ -1,5 +1,5 @@ import _ from 'lodash' -import { createTransformer, toJS, extendObservable, observable, computed, action } from 'mobx' +import { createTransformer, toJS, extendObservable, observable, computed, action, when } from 'mobx' import config from 'config' import { syncFile } from './actions' @@ -22,6 +22,12 @@ const state = observable({ }, }) +// for plugin +const extState = observable({ + createdListeners: [], + didOpenListeners: [], +}) + // state.entities.intercept((change) => { // console.log(change) // if (change.type === 'add') { @@ -43,6 +49,12 @@ class FileNode { }) } state.entities.set(this.path, this) + // 文件创建钩子 + if (extState.createdListeners && extState.createdListeners.length > 0) { + for (const createdListener of extState.createdListeners) { + createdListener(this) + } + } } @action @@ -57,6 +69,7 @@ class FileNode { @observable isSynced = true @observable gitStatus = 'NONE' @observable size = 0 + @observable isEditorLoading = false @observable _name = undefined @computed @@ -139,10 +152,18 @@ class FileNode { state.entities.set(ROOT_PATH, new FileNode({ path: ROOT_PATH, - name: config.projectName, + name: (config.workspaceName === 'default' ? config.projectName : config.workspaceName) || 'Home', isDir: true, })) +when(() => config.workspaceName || config.projectName, () => { + state.entities.set(ROOT_PATH, new FileNode({ + path: ROOT_PATH, + name: config.workspaceName === 'default' ? config.projectName : config.workspaceName, + isDir: true, + })) +}) + function hydrate (json) { const { entities } = json // hydrate encodings @@ -155,4 +176,4 @@ function hydrate (json) { } export default state -export { state, FileNode, hydrate } +export { state, FileNode, hydrate, extState } diff --git a/app/commons/File/subscribeToFileChange.js b/app/commons/File/subscribeToFileChange.js index 45e58418..41ab862c 100644 --- a/app/commons/File/subscribeToFileChange.js +++ b/app/commons/File/subscribeToFileChange.js @@ -1,6 +1,6 @@ import config from 'config' import api from 'backendAPI' -import emitter, { FILE_CHANGE } from 'utils/emitter' +import emitter, { FILE_CHANGE, OFFLINE_WS_SYSTEM } from 'utils/emitter' import { autorun } from 'mobx' import { FsSocketClient } from 'backendAPI/websocketClients' import store, { getState, dispatch } from 'store' @@ -59,6 +59,9 @@ export default function subscribeToFileChange () { client.subscribe(`/topic/ws/${config.spaceKey}/change`, (frame) => { const data = JSON.parse(frame.body) const node = data.fileInfo + if (node.name.startsWith('.nfs000')) { + return + } emitter.emit(FILE_CHANGE, data) switch (data.changeType) { case 'create': @@ -99,5 +102,13 @@ export default function subscribeToFileChange () { const data = JSON.parse(frame.body) dispatch(GitActions.updateCurrentBranch({ name: data.branch })) }) + + client.subscribe(`/topic/ws/${config.spaceKey}/system`, (frame) => { + const data = frame.body; + if (data === 'quit') { + api.closeWebsocketClient() + emitter.emit(OFFLINE_WS_SYSTEM) + } + }) }) } diff --git a/app/commons/Menu/state.js b/app/commons/Menu/state.js index 9679c743..457f8d13 100644 --- a/app/commons/Menu/state.js +++ b/app/commons/Menu/state.js @@ -1,19 +1,18 @@ -import React from 'react' import { observable, extendShallowObservable } from 'mobx' -import keyMapConfig, { modifierKeysMap } from 'commands/keymaps' +import { modifierKeysMap, getFlattenAllKeymaps } from 'commands/keymaps' -const findKeyByValue = value => Object - .keys(keyMapConfig) - .reduce((p, v) => { - p[keyMapConfig[v]] = v +function MenuScope (defaultMenuItems = []) { + const findKeyByValue = value => + Object.keys(getFlattenAllKeymaps()).reduce((p, v) => { + p[getFlattenAllKeymaps()[v]] = v return p }, {})[value] || '' -const withModifierKeys = value => value.split('+') - .map(e => modifierKeysMap[e] || e.toUpperCase()).join('') - - -function MenuScope (defaultMenuItems=[]) { + const withModifierKeys = value => + value + .split('+') + .map(e => modifierKeysMap[e] || e.toUpperCase()) + .join('') class MenuItem { constructor (opts) { @@ -28,7 +27,7 @@ function MenuScope (defaultMenuItems=[]) { return this.items.reduce((acc, item) => { if (item.key) acc[item.key] = item }, {}) - }, + } }) // case 'key': // case 'name': @@ -43,7 +42,7 @@ function MenuScope (defaultMenuItems=[]) { }) return extendShallowObservable(this, { get shortcut () { - return withModifierKeys(findKeyByValue(this.command)) + return withModifierKeys(findKeyByValue(this.cmd || this.command)) } }) } diff --git a/app/commons/Search/action.js b/app/commons/Search/action.js new file mode 100644 index 00000000..5334b4af --- /dev/null +++ b/app/commons/Search/action.js @@ -0,0 +1,62 @@ +import * as api from 'backendAPI/searchAPI' +import state from './state' + +function searchUp() { + if(!state.ws.status) { + state.ws.name = spaceKey; + api.searchWorkspaceUp(); + } +} + +function searchDown() { + if(state.ws.status) { + api.searchWorkspaceDown(); + } +} + +function searchStatus() { + api.searchWorkspaceStatus(); +} + +function searchString() { + if(state.ws.status) { + commonSearch(); + api.searchString(state.searching, state.searched.randomKey); + } +} + +function searchPattern() { + if(state.ws.status) { + commonSearch(); + api.searchPattern(state.searching, state.searched.randomKey); + } +} + +function commonSearch() { + // if(!state.searched.end && !state.ws.first) { + // // api.searchInterrupt(state.searched.taskId); + // } + if(state.searched.taskId != null) { + state.searched.former.taskId = state.searched.taskId + state.searched.former.results = state.searched.results.splice(0, state.searched.results.length); + state.searched.taskId = '' + } + state.searched.taskId = '' + state.searched.pattern = state.searching.isPattern + state.searched.message = '' + state.searched.end = false + state.searched.randomKey = randomForOne() +} + +function searchInterrupt() { + if(state.ws.status + && !state.searched.end) { + api.searchInterrupt(state.searched.taskId); + } +} + +function randomForOne() { + return Math.random().toString(36) +} + +export { searchUp, searchDown, searchStatus, searchString, searchPattern, searchInterrupt } \ No newline at end of file diff --git a/app/commons/Search/state.js b/app/commons/Search/state.js new file mode 100644 index 00000000..58f1e6b2 --- /dev/null +++ b/app/commons/Search/state.js @@ -0,0 +1,38 @@ +import { observable, map } from 'mobx' + +export const ws = observable({ + name: '', + status: false, + first: true +}) + +export const searching = observable({ + pattern: '', + path: '', + caseSensitive: false, + word: false, + isPattern: false, + singleFork: false, + tip: '' +}) + +export const searched = observable({ + taskId: '', + randomKey: '', + pattern: false, + message: '', + results: [], + end: false, + former: { + taskId: '', + results: [] + } +}) + +const state = { + ws, + searching, + searched +} + +export default state; \ No newline at end of file diff --git a/app/commons/Search/subscribeToSearch.js b/app/commons/Search/subscribeToSearch.js new file mode 100644 index 00000000..dee08289 --- /dev/null +++ b/app/commons/Search/subscribeToSearch.js @@ -0,0 +1,153 @@ +import { SearchSocketClient } from 'backendAPI/websocketClients' +import { autorun } from 'mobx' +import state from './state' +import * as api from 'backendAPI/searchAPI' +import debounce from 'lodash/debounce' + + +export default function subscribeToSearch() { + autorun(() => { + + if (!config.searchSocketConnected) return + const client = SearchSocketClient.$$singleton.stompClient + + // workspace up + client.subscribe('/user/topic/ws/up', response => { + let result = JSON.parse(response.body); + editWsStatus(result, 'UP'); + }); + + // workspace down + client.subscribe('/user/topic/ws/down', response => { + let result = JSON.parse(response.body); + editWsStatus(result, 'DOWN'); + }); + + // workspace status + client.subscribe('/user/topic/ws/status', response => { + let result = JSON.stringify(response.body); + if(result) { + switch(result) { + case 'UP': + state.ws.status = true; + break + case 'DOWN': + state.ws.status = false; + break + case 'NULL': + } + } + }); + + // search string + client.subscribe('/user/topic/search/string', response => { + formatSearch(response, false); + }); + + // search pattern + client.subscribe('/user/topic/search/pattern', response => { + formatSearch(response, true); + }); + + // single result + client.subscribe('/user/topic/search/single', response => { + let result = JSON.parse(response.body); + if(result == null) { + return ; + } + setData(result); + }); + + // multi result + client.subscribe('/user/topic/search/join', response => { + let results = JSON.parse(response.body); + if(results == null) { + return ; + } + setData(results); + }); + + // error + client.subscribe('/user/topic/search/error', response => { + let results = JSON.parse(response.body); + if(results == null) { + return ; + } + if(result.randomKey === state.searched.randomKey) { + state.searched.message = results.message; + } + }); + + // end result + client.subscribe('/user/topic/search/end', response => { + end(response) + }); + + // modify workspace status to up and init workspace files in mem + api.searchWorkspaceUp(); + + }) +} + +// end +const end = debounce((response) => { + let result = JSON.parse(response.body); + if(result == null) { + return ; + } + if(result.randomKey === state.searched.randomKey) { + state.searched.end = true; + } + }, 500) + +function setData(result) { + if(result.randomKey === state.searched.randomKey) { + if(result.joinResultMessage) { + let {joinResultMessage: searchChunk} = result; + for(let chunk of searchChunk) { + state.searched.results.push(chunk); + } + } else if(result.singleResultMessage) { + let {singleResultMessage: searchChunk} = result; + state.searched.results.push(searchChunk); + } + } +} + +function editWsStatus(result, wsStatus) { + if(result == null) { + return ; + } + if(result.code == 0 || result.code == 2) { + switch(wsStatus) { + case 'UP': + config.searchWsStatus = true; + state.ws.status = true; + break; + case 'DOWN': + config.searchWsStatus = false; + state.ws.status = false; + break; + } + } else if(result.code == 1) { + console.log(result.message); + } +} + +function formatSearch(response, isPattern) { + let result = JSON.parse(response.body); + if(result == null) { + return ; + } + if(result.randomKey && result.randomKey === state.searched.randomKey) { + if(result.message) { + state.searched.message = result.message + state.searched.end = true + } else { + state.searched.taskId = result.taskId; + state.searched.isPattern = isPattern; + state.searched.message = ''; + state.searched.end = false; + } + } +} \ No newline at end of file diff --git a/app/commons/Tab/TabBar.jsx b/app/commons/Tab/TabBar.jsx index 70eaea38..969defb9 100644 --- a/app/commons/Tab/TabBar.jsx +++ b/app/commons/Tab/TabBar.jsx @@ -1,6 +1,5 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' -import cx from 'classnames' import { observer } from 'mobx-react' import { dnd } from 'utils' import { defaultProps } from 'utils/decorators' @@ -8,7 +7,10 @@ import TabLabel from './TabLabel' import Menu from 'components/Menu' import ContextMenu from 'components/ContextMenu' import i18n from 'utils/createI18n' - +import * as SideBar from 'components/Panel/SideBar/actions' +import config from 'config' +import * as Panel from 'components/Panel/actions' +import panelState from 'components/Panel/state' @defaultProps(props => ({ addTab: () => props.tabGroup.addTab(), @@ -20,6 +22,8 @@ class TabBar extends Component { contextMenuItems: PropTypes.array.isRequired, addTab: PropTypes.func, closePane: PropTypes.func, + fullScreenActiveContent: PropTypes.bool, + handleFullScreen: PropTypes.func, }; constructor (props) { @@ -37,18 +41,27 @@ class TabBar extends Component { tabGroup, addTab, contextMenuItems, + fullScreenActiveContent, + handleFullScreen, } = this.props const tabBarId = `tab_bar_${tabGroup.id}` + const size = panelState.panels.get('PANEL_BOTTOM').size + return (
    {tabGroup.tabs.map(tab => - + )}
{dnd.target.id === tabBarId ?
: null} @@ -57,12 +70,29 @@ class TabBar extends Component {
+
+ {(!config.isPad || tabGroup.id !== 'terminalGroup') + &&
{ + e.stopPropagation() + this.setState({ showDropdownMenu: !this.state.showDropdownMenu }) + }} + > + + {this.renderDropdownMenu(size)} +
} + {tabGroup.id === 'terminalGroup' &&
{ e.stopPropagation(); this.setState({ showDropdownMenu: true }) }} + style={{ marginBottom: size ? '4px' : '-6px' }} + onClick={e => { + e.stopPropagation() + !size ? Panel.expandPanel('PANEL_BOTTOM') + : Panel.shrinkPanel('PANEL_BOTTOM') + }} > - - {this.renderDropdownMenu()} -
+ +
} this.setState({ showDropdownMenu: false })} />) } diff --git a/app/commons/Tab/TabContent.jsx b/app/commons/Tab/TabContent.jsx index 9af96bc2..dd1f3da7 100644 --- a/app/commons/Tab/TabContent.jsx +++ b/app/commons/Tab/TabContent.jsx @@ -10,11 +10,12 @@ export const TabContent = ({ children }) => ( ) export const TabContentItem = observer(({ tab, children }) => { + const ext = tab.title && tab.title.split('.')[1]; if (tab.isActive && tab.onActive) { tab.onActive() } return ( -
+
{children}
) @@ -23,4 +24,3 @@ export const TabContentItem = observer(({ tab, children }) => { TabContentItem.propTypes = { tab: PropTypes.object.isRequired, } - diff --git a/app/commons/Tab/TabLabel.jsx b/app/commons/Tab/TabLabel.jsx index 752bd19f..4f872516 100644 --- a/app/commons/Tab/TabLabel.jsx +++ b/app/commons/Tab/TabLabel.jsx @@ -5,29 +5,114 @@ import { observer } from 'mobx-react' import { dnd } from 'utils' import { defaultProps } from 'utils/decorators' import { dispatch } from '../../store' +import * as Modal from '../../components/Modal/actions' +import config from 'config' +import dispatchCommand from 'commands/dispatchCommand' +import { fileIconProviders } from 'components/FileTree/state' +import { gtouchstart, gtouchend, gtouchmove } from 'utils/touch' -let TabLabel = observer(({tab, removeTab, activateTab, openContextMenu}) => { +const closeFileTab = async (e, tab, removeTab, activateTab) => { + e.stopPropagation() + if (tab.tabGroupId === 'terminalGroup') { + removeTab(tab.id) + return + } + activateTab(tab.id) + const editor = tab.editorInfo.monacoEditor + + const content = (editor && editor.getValue) ? editor.getValue() : '' + + if (!tab.file && content && tab.editorInfo.uri.includes('inmemory')) { + const confirmed = await Modal.showModal('Confirm', { + header: i18n`file.saveNew`, + message: i18n`file.newInfo`, + }) + if (confirmed) { + Modal.dismissModal() + dispatchCommand('file:save') + } else { + Modal.dismissModal() + removeTab(tab.id) + } + } else { + removeTab(tab.id) + } +} + +const TabIcon = observer(({ fileName, defaultIconStr }) => { + const provider = fileIconProviders.get(config.fileicons) + const fileIconsProvider = typeof provider === 'function' ? provider() : provider + if (fileIconsProvider && fileIconsProvider.fileicons) { + const { icons: allicons, fileicons: fileiconsMap } = fileIconsProvider + if (!fileiconsMap.icons || fileiconsMap.icons.length === 0) { + return () + } + let fileiconName = fileiconsMap.defaultIcon + const extension = fileName.split('.').pop() + for (let i = 0; i < fileiconsMap.icons.length; i += 1) { + const fileicon = fileiconsMap.icons[i] + if ((fileicon.fileNames && fileicon.fileNames.includes(fileName)) || (fileicon.fileExtensions && fileicon.fileExtensions.includes(extension))) { + fileiconName = fileicon.name + break + } + } + return ( + + ) + } + return
+}) + +let TabLabel = observer(({ tab, removeTab, activateTab, openContextMenu, dbClickHandler }) => { const tabLabelId = `tab_label_${tab.id}` return (
  • activateTab(tab.id)} + onClick={e => { activateTab(tab.id) }} + onMouseUp={e => { e.button === 1 && removeTab(tab.id) }} + onDoubleClick={() => { + if (!tab.isActive) { + activateTab(tab.id) + } + dbClickHandler() + }} onDragStart={e => { // Chrome 下直接执行 dragStart 会导致立即又出发了 window.dragend, 添加 timeout 以避免无法拖动的情况 setTimeout(() => dnd.dragStart({ type: 'TAB', id: tab.id }), 0) }} - onContextMenu={e => openContextMenu(e, tab)} + onContextMenu={e => config.isPad ? '' : openContextMenu(e, tab)} + onTouchStart={e => { + e.persist() + gtouchstart(() => { openContextMenu(e, tab) }) + }} + onTouchEnd={gtouchend} + onTouchMove={gtouchmove} > - {dnd.target.id === tabLabelId ?
    : null} - {tab.icon ?
    : null} + {dnd.target.id === tabLabelId ?
    : null} + {tab.icon && }
    {tab.title}
    - { e.stopPropagation(); removeTab(tab.id) }}>× + closeFileTab(e, tab, removeTab, activateTab)}>×
  • @@ -39,6 +124,7 @@ TabLabel.propTypes = { removeTab: PropTypes.func.isRequired, activateTab: PropTypes.func.isRequired, openContextMenu: PropTypes.func.isRequired, + dbClickHandler: PropTypes.func.isRequired, } TabLabel = defaultProps(props => ({ diff --git a/app/commons/Tab/state.js b/app/commons/Tab/state.js index d6260df4..5da9f819 100644 --- a/app/commons/Tab/state.js +++ b/app/commons/Tab/state.js @@ -32,54 +32,55 @@ function TabScope () { class Tab { - @observable _title = i18n.get('tab.makeDropdownMenuItems.untitledTab') - @computed - get title () { return this._title } - set title (v) { return this._title = v } - - @observable index = 0 - @observable tabGroupId = '' - @observable flags = {} + @observable _title = i18n.get('tab.makeDropdownMenuItems.untitledTab') + @computed + get title () { return this._title } + set title (v) { return this._title = v } + + @observable index = 0 + @observable tabGroupId = '' + @observable flags = {} + @observable type = '' + + @computed get tabGroup () { + return state.tabGroups.get(this.tabGroupId) + } - @computed get tabGroup () { - return state.tabGroups.get(this.tabGroupId) - } + @computed get isActive () { + return this.tabGroup && this.tabGroup.activeTab === this + } - @computed get isActive () { - return this.tabGroup && this.tabGroup.activeTab === this - } + @computed get siblings () { + return this.tabGroup.tabs + } - @computed get siblings () { - return this.tabGroup.tabs - } + @computed get next () { + return this.siblings[this.index + 1] + } - @computed get next () { - return this.siblings[this.index + 1] - } + @computed get prev () { + return this.siblings[this.index - 1] + } - @computed get prev () { - return this.siblings[this.index - 1] - } + getAdjacent (checkNextFirst) { + const adjacent = checkNextFirst ? + (this.next || this.prev) : (this.prev || this.next) + return adjacent + } - getAdjacent (checkNextFirst) { - const adjacent = checkNextFirst ? - (this.next || this.prev) : (this.prev || this.next) - return adjacent + @action activate () { + this.tabGroup.activeTabId = this.id + this.tabGroup.activate() } - @action activate () { - this.tabGroup.activeTabId = this.id - this.tabGroup.activate() - } - - @action destroy () { - if (state.tabs.size === 1 && state.keepOne) { - return + @action destroy () { + if (state.tabs.size === 1 && state.keepOne) { + return + } + this.tabGroup.removeTab(this) + state.tabs.delete(this.id) } - this.tabGroup.removeTab(this) - state.tabs.delete(this.id) } -} autorun(() => { state.tabGroups.forEach((tabGroup) => { @@ -93,106 +94,106 @@ function TabScope () { class TabGroup { static Tab = Tab; - @observable activeTabId = null + @observable activeTabId = null - @computed get tabs () { - return state.tabs.values() - .filter(tab => tab.tabGroupId === this.id) - .sort((a, b) => a.index - b.index) - } + @computed get tabs () { + return state.tabs.values() + .filter(tab => tab.tabGroupId === this.id) + .sort((a, b) => a.index - b.index) + } - @computed get activeTab () { - const activeTab = state.tabs.get(this.activeTabId) - if (activeTab && activeTab.tabGroupId === this.id) { - return activeTab - } else if (this.tabs.length > 0) { - return this.tabs[0] + @computed get activeTab () { + const activeTab = state.tabs.get(this.activeTabId) + if (activeTab && activeTab.tabGroupId === this.id) { + return activeTab + } else if (this.tabs.length > 0) { + return this.tabs[0] + } + return null } - return null - } - @computed get siblings () { - return state.tabGroups.values() - } + @computed get siblings () { + return state.tabGroups.values() + } - @computed get isActive () { - return state.activeTabGroup === this - } + @computed get isActive () { + return state.activeTabGroup === this + } - @mapEntity('tabs') - @action addTab (tab, insertIndex = this.tabs.length) { - if (!tab) tab = new this.constructor.Tab() - if (tab.tabGroupId === this.id) { - if (tab.index === undefined) { - tab.index = insertIndex - } else if (tab.index === insertIndex) { - return + @mapEntity('tabs') + @action addTab (tab, insertIndex = this.tabs.length) { + if (!tab) tab = new this.constructor.Tab() + if (tab.tabGroupId === this.id) { + if (tab.index === undefined) { + tab.index = insertIndex + } else if (tab.index === insertIndex) { + return + } else { + this.tabs.map((tabItem) => { + if (tab.index > insertIndex) { + if (tabItem.index >= insertIndex && tabItem.index < tab.index) { + tabItem.index ++ + } + } else { + if (tabItem.index <= insertIndex && tabItem.index > tab.index) { + tabItem.index -- + } + } + }) + tab.index = insertIndex > 0 ? insertIndex : 0 + } } else { this.tabs.map((tabItem) => { - if (tab.index > insertIndex) { - if (tabItem.index >= insertIndex && tabItem.index < tab.index) { - tabItem.index ++ - } - } else { - if (tabItem.index <= insertIndex && tabItem.index > tab.index) { - tabItem.index -- - } + if (tabItem.index >= insertIndex && tabItem.index < tab.index) { + tabItem.index ++ } }) tab.index = insertIndex > 0 ? insertIndex : 0 } - } else { - this.tabs.map((tabItem) => { - if (tabItem.index >= insertIndex && tabItem.index < tab.index) { - tabItem.index ++ - } - }) - tab.index = insertIndex > 0 ? insertIndex : 0 + + tab.tabGroupId = this.id + tab.activate() + return tab } - - tab.tabGroupId = this.id - tab.activate() - return tab - } - @action activate () { - state.activeTabGroupId = this.id - } + @action activate () { + state.activeTabGroupId = this.id + } - @mapEntity('tabs') - @action activateTab (tab) { - tab.activate() - } + @mapEntity('tabs') + @action activateTab (tab) { + tab.activate() + } - @mapEntity('tabs') - @action removeTab (tab) { - if (tab.isActive) { - const adjacentTab = tab.getAdjacent() - if (adjacentTab) adjacentTab.activate() + @mapEntity('tabs') + @action removeTab (tab) { + if (tab.isActive) { + const adjacentTab = tab.getAdjacent() + if (adjacentTab) adjacentTab.activate() + } + tab.tabGroupId = null } - tab.tabGroupId = null - } - @mapEntity('tabGroups') - @action merge (tabGroup) { - if (!tabGroup) return - const baseIndex = this.tabs.length - tabGroup.tabs.forEach((tab) => { - tab.tabGroupId = this.id - tab.index += baseIndex - }) - this.activate() - } + @mapEntity('tabGroups') + @action merge (tabGroup) { + if (!tabGroup) return + const baseIndex = this.tabs.length + tabGroup.tabs.forEach((tab) => { + tab.tabGroupId = this.id + tab.index += baseIndex + }) + this.activate() + } - @mapEntity('tabGroups') - @action mergeTo (tabGroup) { - tabGroup.merge(this) - } + @mapEntity('tabGroups') + @action mergeTo (tabGroup) { + tabGroup.merge(this) + } - @action destroy () { - state.tabGroups.delete(this.id) + @action destroy () { + state.tabGroups.delete(this.id) + } } -} return { Tab, TabGroup, state } } diff --git a/app/commons/Tree/TreeNode.jsx b/app/commons/Tree/TreeNode.jsx index 557109fd..5c0b9001 100644 --- a/app/commons/Tree/TreeNode.jsx +++ b/app/commons/Tree/TreeNode.jsx @@ -1,43 +1,157 @@ import React, { Component } from 'react' +import { camelCase } from 'lodash' import ReactCSSTransitionGroup from 'react-addons-css-transition-group' import { observer } from 'mobx-react' import config from 'config' import cx from 'classnames' import dnd from 'utils/dnd' +import { gtouchstart, gtouchend, gtouchmove } from 'utils/touch' import icons from 'file-icons-js' +import { fileIconProviders } from 'components/FileTree/state' @observer class TreeNode extends Component { - constructor (props) { - super(props) + componentDidUpdate () { + if (this.props.node.isFocused && this.nodeDOM) { + this.nodeDOM.scrollIntoViewIfNeeded && this.nodeDOM.scrollIntoViewIfNeeded() + } + } + + resolveFolderIcon = (defaultIconStr) => { + const { node } = this.props + const provider = fileIconProviders.get(config.fileicons) + const fileIconsProvider = typeof provider === 'function' ? provider() : provider + if (fileIconsProvider && fileIconsProvider.foldericons) { + const { icons: allicons, foldericons: foldericonsMap } = fileIconsProvider + if (!foldericonsMap.icons || foldericonsMap.icons.length === 0) { + return () + } + let folderName = foldericonsMap.defaultIcon + for (let i = 0; i < foldericonsMap.icons.length; i += 1) { + const foldericon = foldericonsMap.icons[i] + if (foldericon.folderNames.includes(node.name)) { + folderName = foldericon.name + break + } + } + const finalyFolderName = !!node.isFolded + ? allicons[folderName] ? folderName : foldericonsMap.defaultIcon + : allicons[`${folderName}-open`] ? `${folderName}-open` : `${foldericonsMap.defaultIcon}-open` + return ( + + ) + } + return ( + + + + ) + } + + resolveFileIcon = (defaultIconStr) => { + const { node } = this.props + const provider = fileIconProviders.get(config.fileicons) + const fileIconsProvider = typeof provider === 'function' ? provider() : provider + if (fileIconsProvider && fileIconsProvider.fileicons) { + const { icons: allicons, fileicons: fileiconsMap } = fileIconsProvider + if (!fileiconsMap.icons || fileiconsMap.icons.length === 0) { + return () + } + let fileiconName = fileiconsMap.defaultIcon + const extension = node.name.split('.').pop() + for (let i = 0; i < fileiconsMap.icons.length; i += 1) { + const fileicon = fileiconsMap.icons[i] + if ((fileicon.fileNames && fileicon.fileNames.includes(node.name)) || (fileicon.fileExtensions && fileicon.fileExtensions.includes(extension))) { + fileiconName = fileicon.name + break + } + } + return ( + + ) + } + return ( + + + + ) } render () { const { node, openNode, selectNode, openContextMenu, onlyDir } = this.props if (!node || node.parentId === undefined) return null if (onlyDir && !node.isDir) return null + // const fileIconProvider = fileIconProviders.get('material-file-icon')() || null + const isDefaultIcon = config.fileicons === 'default' let iconStr = '' - if (node.isRoot) { - iconStr = 'fa fa-briefcase' - } else if (node.isDir && !node.isRoot && node.isFolded) { + if (!node.isDir) { + iconStr = icons.getClassWithColor(node.name) || 'fa fa-file-text-o' + } else if (node.isFolded) { iconStr = 'fa fa-folder-o' - } else if (node.isDir && !node.isRoot && !node.isFolded) { + } else if (!node.isFolded) { iconStr = 'fa fa-folder-open-o' - } else if (!node.isDir) { - iconStr = icons.getClassWithColor(node.name) || 'fa fa-file-text-o' } + // if (node.isRoot) { + // iconStr = 'fa fa-briefcase' + // } else if (node.isDir && !node.isRoot && node.isFolded) { + // iconStr = 'fa fa-folder-o' + // } else if (node.isDir && !node.isRoot && !node.isFolded) { + // iconStr = 'fa fa-folder-open-o' + // } else if (!node.isDir) { + // iconStr = icons.getClassWithColor(node.name) || 'fa fa-file-text-o' + // } return ( -
    { + data-droppable='FILE_TREE_NODE' + onContextMenu={(e) => { + if (config.isPad) return selectNode(node) openContextMenu(e, node) }} + onTouchMove={gtouchmove} + onTouchEnd={gtouchend} + onTouchStart={e => { + e.persist() + gtouchstart(() => { + selectNode(node) + openContextMenu(e, node) + }) + e.stopPropagation() + }} draggable='true' - onDragStart={e => { + onDragStart={(e) => { e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text/html', e.target.innerHTML) e.stopPropagation() @@ -46,57 +160,76 @@ class TreeNode extends Component { } }} > -
    this.nodeDOM = r} +
    (this.nodeDOM = r)} + onClick={e => { + selectNode(node) + config.isPad ? openNode(node) : '' + }} onDoubleClick={e => openNode(node)} - onClick={e => selectNode(node)} style={{ paddingLeft: `${1 + node.depth}em` }} > {node.isLoading && } - {!node.isLoading && openNode(node, null, e.altKey)}> - {node.isDir && } - } - - - - + {!node.isLoading && ( + e.stopPropagation()} + onClick={e => config.isPad ? '' : openNode(node, null, e.altKey)} + > + {node.isDir && ( + + )} + + )} + {isDefaultIcon ? ( + {} + ) : node.isDir ? ( + this.resolveFolderIcon(iconStr) + ) : ( + this.resolveFileIcon(iconStr) + )} + {node.name || config.projectName}
    - {node.isDir && -
    - - {!node.isFolded && node.children.length > 0 &&
    - {node.children.map(childNode => - - )} -
    } -
    -
    } + {node.isDir && ( +
    + + {!node.isFolded && + node.children.length > 0 && ( +
    + {node.children.map(childNode => ( + + ))} +
    + )} +
    +
    + )}
    ) } - - componentDidUpdate () { - if (this.props.node.isFocused && this.nodeDOM) { - this.nodeDOM.scrollIntoViewIfNeeded && this.nodeDOM.scrollIntoViewIfNeeded() - } - } } export default TreeNode diff --git a/app/commons/Tree/state.js b/app/commons/Tree/state.js index 2d569af6..60a49fbf 100644 --- a/app/commons/Tree/state.js +++ b/app/commons/Tree/state.js @@ -31,153 +31,153 @@ function TreeNodeScope () { state.entities.set(this.id, this) } - @protectedObservable _name = '' - @protectedObservable _isDir = false - @protectedObservable _parentId = undefined - - @observable _depth = undefined - @observable isFolded = true - @observable isLoading = false - @observable isFocused = false - @observable isHighlighted = false - @observable index = 0 - - @computed get isShadowRoot () { - return this.id === SHADOW_ROOT_NODE - } + @protectedObservable _name = '' + @protectedObservable _isDir = false + @protectedObservable _parentId = undefined + + @observable _depth = undefined + @observable isFolded = true + @observable isLoading = false + @observable isFocused = false + @observable isHighlighted = false + @observable index = 0 + + @computed get isShadowRoot () { + return this.id === SHADOW_ROOT_NODE + } - @computed get parent () { - let parent - if (typeof this.parentId === 'string') parent = state.entities.get(this.parentId) - // don't allow recurse to self - if (this.id === SHADOW_ROOT_NODE) return null - if (parent === this) { throw Error(`Node ${this.id} is parent of itself...`) } - return parent || state.shadowRoot - } - set parent (parent) { this.parentId = parent.id } + @computed get parent () { + let parent + if (typeof this.parentId === 'string') parent = state.entities.get(this.parentId) + // don't allow recurse to self + if (this.id === SHADOW_ROOT_NODE) return null + if (parent === this) { throw Error(`Node ${this.id} is parent of itself...`) } + return parent || state.shadowRoot + } + set parent (parent) { this.parentId = parent.id } - @computed get depth () { - if (this._depth !== undefined) return this._depth - if (this.isShadowRoot) return -1 - return this.parent.depth + 1 - } + @computed get depth () { + if (this._depth !== undefined) return this._depth + if (this.isShadowRoot) return -1 + return this.parent.depth + 1 + } - @computed get children () { - return state.entities.values() - .filter(node => node.parent === this) - .sort(this.constructor.nodeSorter) - } + @computed get children () { + return state.entities.values() + .filter(node => node.parent === this) + .sort(this.constructor.nodeSorter) + } - @computed get siblings () { - return this.parent.children - } + @computed get siblings () { + return this.parent.children + } - @computed get prev () { - return this.siblings[this.index - 1] - } + @computed get prev () { + return this.siblings[this.index - 1] + } - @computed get next () { - if (this.isShadowRoot) return this - return this.siblings[this.index + 1] - } + @computed get next () { + if (this.isShadowRoot) return this + return this.siblings[this.index + 1] + } - @computed get firstChild () { - return this.children[0] - } + @computed get firstChild () { + return this.children[0] + } - @computed get lastChild () { - return this.children[this.children.length - 1] - } + @computed get lastChild () { + return this.children[this.children.length - 1] + } - @computed get lastVisibleDescendant () { - const lastChild = this.lastChild - if (!lastChild) return this - if (!lastChild.isDir) return lastChild - if (lastChild.isFolded) return lastChild - return lastChild.lastVisibleDescendant - } + @computed get lastVisibleDescendant () { + const lastChild = this.lastChild + if (!lastChild) return this + if (!lastChild.isDir) return lastChild + if (lastChild.isFolded) return lastChild + return lastChild.lastVisibleDescendant + } - @computed get getPrev () { - if (this.isShadowRoot) return this - const prevNode = this.prev - if (prevNode) { - if (!prevNode.isDir || prevNode.isFolded) return prevNode - if (prevNode.lastChild) { - return prevNode.lastVisibleDescendant + @computed get getPrev () { + if (this.isShadowRoot) return this + const prevNode = this.prev + if (prevNode) { + if (!prevNode.isDir || prevNode.isFolded) return prevNode + if (prevNode.lastChild) { + return prevNode.lastVisibleDescendant + } + return prevNode } - return prevNode + return this.parent } - return this.parent - } - @computed get getNext () { - if (this.isDir && !this.isFolded) { - if (this.firstChild) return this.firstChild - } else if (this.isShadowRoot) { - return this - } + @computed get getNext () { + if (this.isDir && !this.isFolded) { + if (this.firstChild) return this.firstChild + } else if (this.isShadowRoot) { + return this + } - const nextNode = this.next - if (nextNode) return nextNode - return this.parent.next - } + const nextNode = this.next + if (nextNode) return nextNode + return this.parent.next + } - @action forEachDescendant (handler) { - if (!this.isDir) return - this.children.forEach((childNode, i) => { - handler(childNode, i) - childNode.forEachDescendant(handler) - }) - } + @action forEachDescendant (handler) { + if (!this.isDir) return + this.children.forEach((childNode, i) => { + handler(childNode, i) + childNode.forEachDescendant(handler) + }) + } - @action focus () { - if (this.isShadowRoot) return - this.isFocused = true - } + @action focus () { + if (this.isShadowRoot) return + this.isFocused = true + } - @action unfocus () { - this.isFocused = false - } + @action unfocus () { + this.isFocused = false + } - @action fold () { - if (!this.isDir || this.isFolded) return - this.isFolded = true - } + @action fold () { + if (!this.isDir || this.isFolded) return + this.isFolded = true + } - @action unfold () { - if (!this.isDir || !this.isFolded) return - this.isFolded = false - } + @action unfold () { + if (!this.isDir || !this.isFolded) return + this.isFolded = false + } - @action toggleFold (shouldBeFolded) { - if (shouldBeFolded) { - this.fold() - } else { - this.unfold() + @action toggleFold (shouldBeFolded) { + if (shouldBeFolded) { + this.fold() + } else { + this.unfold() + } } - } - @action highlight () { - if (!this.isDir || this.isHighlighted) return - this.isHighlighted = true - } + @action highlight () { + if (!this.isDir || this.isHighlighted) return + this.isHighlighted = true + } - @action unhighlight () { - if (!this.isDir || !this.isHighlighted) return - this.isHighlighted = false - } + @action unhighlight () { + if (!this.isDir || !this.isHighlighted) return + this.isHighlighted = false + } - @action exile () { - this.originalParentId = this._parentId - this._parentId = NOWHERE_LAND - } + @action exile () { + this.originalParentId = this._parentId + this._parentId = NOWHERE_LAND + } - @action welcome () { - if (this._parentId !== NOWHERE_LAND) return - this._parentId = this.originalParentId - delete this.originalParentId + @action welcome () { + if (this._parentId !== NOWHERE_LAND) return + this._parentId = this.originalParentId + delete this.originalParentId + } } -} const shadowRootNode = new TreeNode({ id: SHADOW_ROOT_NODE, diff --git a/app/components/Accordion/Accordion.jsx b/app/components/Accordion/Accordion.jsx index 4981cc21..404c4dbe 100644 --- a/app/components/Accordion/Accordion.jsx +++ b/app/components/Accordion/Accordion.jsx @@ -16,6 +16,7 @@ class Accordion extends Component { const style = { flexGrow: this.state.showSection ? this.props.size : 0, } + const { bodyStyle } = this.props return (
    @@ -30,7 +31,7 @@ class Accordion extends Component { {this.props.actions}
    -
    +
    {this.props.children}
    diff --git a/app/components/ContextMenu/store.js b/app/components/ContextMenu/store.js index 3d380b06..26a4972d 100644 --- a/app/components/ContextMenu/store.js +++ b/app/components/ContextMenu/store.js @@ -2,6 +2,7 @@ import state from './state' import isFunction from 'lodash/isFunction' import isArray from 'lodash/isArray' import { createAction, handleAction, registerAction } from 'utils/actions' +import config from 'config' const OPEN_CONTEXT_MENU = 'menu:open_context_menu' handleAction(OPEN_CONTEXT_MENU, ({ isActive, pos, contextNode, items, className }) => { @@ -23,7 +24,14 @@ function openContextMenuFactory (items, margin, className) { const openContextMenu = createAction(OPEN_CONTEXT_MENU, (e, context, items = [], margin = { x: 0, y: 0, relative: false }, className = '') => { e.stopPropagation() e.preventDefault() - let pos = { x: e.clientX + margin.x, y: e.clientY + margin.y } + + const rects = e.target.getBoundingClientRect() + const isPad = config.isPad + + let pos = { + x: (isPad ? rects.x + rects.width / 2 : e.clientX) + margin.x, + y: (isPad ? rects.y + rects.height : e.clientY) + + margin.y + } if (margin.relative) { const rect = e.target.getBoundingClientRect() pos = { x: rect.x + rect.width + margin.x, y: rect.y + rect.height + margin.y } diff --git a/app/components/Editor/EditorWrapper.jsx b/app/components/Editor/EditorWrapper.jsx index c0867dfe..30536fc2 100644 --- a/app/components/Editor/EditorWrapper.jsx +++ b/app/components/Editor/EditorWrapper.jsx @@ -3,33 +3,96 @@ import PropTypes from 'prop-types' import { observer } from 'mobx-react' import { when } from 'mobx' import CodeEditor from './components/CodeEditor' -import MarkdownEditor from './components/MarkdownEditor' +import MarkDownEditor from './components/MarkdownEditor' import ImageEditor from './components/ImageEditor' import UnknownEditor from './components/UnknownEditor' import WelcomeEditor from './components/WelcomeEditor' import HtmlEditor from './components/HtmlEditor' import config from '../../config' +import pluginStore from '../Plugins/store' + +const editorSet = [ + { + editorType: 'htmlEditor', + editor: HtmlEditor, + }, + { + editorType: 'markdownEditor', + editor: MarkDownEditor, + }, + { + editorType: 'imageEditor', + editor: ImageEditor, + }, + { + editorType: 'textEditor', + editor: CodeEditor, + }, + { + editorType: 'unknownEditor', + editor: UnknownEditor, + }, +]; + +function matchEditorByContentType(editorType, contentType) { + for (let i = 0, n = editorSet.length; i < n; i++) { + const set = editorSet[i]; + if (set.editorType) { + if (set.editorType === editorType) { + return set.editor; + } + } else if (set.mime) { + if (set.mime.includes(contentType)) { + return set.editor; + } + } + } + return UnknownEditor; +} +// 编辑器插件数组 +let pluginArray = []; const EditorWrapper = observer(({ tab, active }) => { + // loading + if (tab.file && tab.file.isEditorLoading) { + return ( +
    + +
    + ) + } const { editor } = tab - const editorType = editor.editorType || 'default' const file = editor.file || {} - // key is crutial here, it decides whether - // the component should re-construct or - // keep using the existing instance. - const key = `editor_${file.path}` - switch (editorType) { - case 'htmlEditor': - return React.createElement(HtmlEditor, { editor, key, tab, active }) - case 'default': - return React.createElement(CodeEditor, { editor, key, tab, active }) - case 'editorWithPreview': - return React.createElement(MarkdownEditor, { editor, key, tab, active }) - case 'imageEditor': - return React.createElement(ImageEditor, { path: file.path, key, tab, active }) - default: - return React.createElement(UnknownEditor, { path: file.path, size: file.size, key, tab, active }) + // 编辑器插件 + if (!pluginArray.length) { + pluginArray = pluginStore.plugins.values().filter(plugin => plugin.label.mime); + for (let i = 0, n = pluginArray.length; i < n; i++) { + const plugin = pluginArray[i]; + editorSet.unshift({ + mime: plugin.label.mime, + editor: plugin.app, + }); + } } + // key is crutial here, it decides whether the component should re-construct + // or keep using the existing instance. + const key = `editor_${file.path}`; + const editorElement = matchEditorByContentType(editor.editorType, editor.contentType); + return React.createElement(editorElement, { editor, key, tab, active, path: file.path, size: file.size }); + // switch (editorType) { + // case 'htmlEditor': + // return React.createElement(HtmlEditor, { editor, key, tab, active }) + // case 'textEditor': + // return React.createElement(CodeEditor, { editor, key, tab, active }) + // case 'imageEditor': + // return React.createElement(ImageEditor, { path: file.path, key, tab, active }) + // case 'markdownEditor': + // return React.createElement(MarkdownEditor, { editor, key, tab, active }) + // case 'unknownEditor': + // return React.createElement(UnknownEditor, { path: file.path, size: file.size, key, tab, active }) + // default: + // return React.createElement(UnknownEditor, { path: file.path, size: file.size, key, tab, active }) + // } }) EditorWrapper.propTypes = { diff --git a/app/components/Editor/actions.js b/app/components/Editor/actions.js index 82944d77..cd6201c3 100644 --- a/app/components/Editor/actions.js +++ b/app/components/Editor/actions.js @@ -8,6 +8,18 @@ const getCurrentCM = () => { return cm } +const getCurrentMonaco = () => { + const { EditorTabState } = mobxStore + const activeTab = EditorTabState.activeTab + const monaco = activeTab ? activeTab.editorInfo.monacoEditor : null + return monaco +} + +export const formatMonacoCode = registerAction('edit:toggle_format_monaco', () => { + const monaco = getCurrentMonaco() + monaco.trigger('format', 'editor.action.formatDocument') +}) + export const formatCode = registerAction('edit:toggle_format', () => { const cm = getCurrentCM() if (!cm) return @@ -32,3 +44,8 @@ export const toggleComment = registerAction('edit:toggle_comment', () => { cm.blockComment(range.from, range.to, { fullLines: false }) } }) + +export const toggleMonacoComment = registerAction('edit:toggle_monaco_comment', () => { + const monacoEditor = getCurrentMonaco() + monacoEditor.trigger('format', 'editor.action.commentLine') +}) diff --git a/app/components/Editor/codemirrorDefaultOptions.js b/app/components/Editor/codemirrorDefaultOptions.js index 828db261..8b56d9ae 100644 --- a/app/components/Editor/codemirrorDefaultOptions.js +++ b/app/components/Editor/codemirrorDefaultOptions.js @@ -40,15 +40,15 @@ extraKeys['Tab'] = function betterTab (cm) { // precisely, these are the default options that we want to override const options = { - theme: 'default', - autofocus: true, + theme: 'material', + autofocus: false, lineNumbers: true, lint: null, matchBrackets: true, autoCloseBrackets: true, dragDrop: false, - trimTrailingWhitespace: undefined, - insertFinalNewline: undefined, + trimTrailingWhitespace: 'off', + insertFinalNewline: 'off', smartIndent: false, extraKeys, } diff --git a/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx b/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx index b87f0131..955bd13b 100644 --- a/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx +++ b/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx @@ -16,7 +16,7 @@ class BaseCodeEditor extends Component { this.highlight = this.highlight.bind(this) } - componentDidMount () { + componentDidMount() { const cm = this.cm this.dom.appendChild(this.cmDOM) @@ -29,7 +29,7 @@ class BaseCodeEditor extends Component { // cm.setCursor({ line: scrollLine, ch: 0 }) // cm.focus() // }, 100) - + emitter.on(E.FILE_HIGHLIGHT, this.highlight) } diff --git a/app/components/Editor/components/CodeEditor/addons/mode/loadMode.js b/app/components/Editor/components/CodeEditor/addons/mode/loadMode.js index 6163759e..f0d60fcf 100644 --- a/app/components/Editor/components/CodeEditor/addons/mode/loadMode.js +++ b/app/components/Editor/components/CodeEditor/addons/mode/loadMode.js @@ -164,6 +164,23 @@ export default function loadMode (mode) { return import( 'codemirror/mode/htmlembedded/htmlembedded.js' ) + case 'application/x-jsp': + case 'application/x-aspx': + { + const tasks = [ + import('codemirror/mode/htmlembedded/htmlembedded.js'), + import('codemirror/mode/clike/clike.js') + ] + return Promise.all(tasks) + } + case 'application/x-erb': + { + const tasks = [ + import('codemirror/mode/htmlembedded/htmlembedded.js'), + import('codemirror/mode/ruby/ruby.js') + ] + return Promise.all(tasks) + } case 'htmlmixed': return import( 'codemirror/mode/htmlmixed/htmlmixed.js' diff --git a/app/components/Editor/components/CodeEditor/addons/mode/modeInfos.js b/app/components/Editor/components/CodeEditor/addons/mode/modeInfos.js index 7ef5c70b..51af2771 100644 --- a/app/components/Editor/components/CodeEditor/addons/mode/modeInfos.js +++ b/app/components/Editor/components/CodeEditor/addons/mode/modeInfos.js @@ -32,7 +32,7 @@ const modeInfos = [ { name: 'Eiffel', mime: 'text/x-eiffel', mode: 'eiffel', ext: ['e'] }, { name: 'Elm', mime: 'text/x-elm', mode: 'elm', ext: ['elm'] }, { name: 'Embedded Javascript', mime: 'application/x-ejs', mode: 'htmlembedded', ext: ['ejs'] }, - { name: 'Embedded Ruby', mime: 'application/x-erb', mode: 'htmlembedded', ext: ['erb'] }, + { name: 'Embedded Ruby', mime: 'application/x-erb', mode: 'application/x-erb', ext: ['erb'] }, { name: 'Erlang', mime: 'text/x-erlang', mode: 'erlang', ext: ['erl'] }, { name: 'Factor', mime: 'text/x-factor', mode: 'factor', ext: ['factor'] }, { name: 'FCL', mime: 'text/x-fcl', mode: 'fcl' }, @@ -49,13 +49,13 @@ const modeInfos = [ { name: 'Haskell (Literate)', mime: 'text/x-literate-haskell', mode: 'haskell-literate', ext: ['lhs'] }, { name: 'Haxe', mime: 'text/x-haxe', mode: 'haxe', ext: ['hx'] }, { name: 'HXML', mime: 'text/x-hxml', mode: 'haxe', ext: ['hxml'] }, - { name: 'ASP.NET', mime: 'application/x-aspx', mode: 'htmlembedded', ext: ['aspx'], alias: ['asp', 'aspx'] }, - { name: 'HTML', mime: 'text/html', mode: 'htmlmixed', ext: ['html', 'htm'], alias: ['xhtml'] }, + { name: 'ASP.NET', mime: 'application/x-aspx', mode: 'application/x-aspx', ext: ['aspx'], alias: ['asp', 'aspx'] }, + { name: 'HTML', mime: 'text/html', mode: 'htmlmixed', ext: ['html', 'htm', 'wpy'], alias: ['xhtml'] }, { name: 'HTTP', mime: 'message/http', mode: 'http' }, { name: 'IDL', mime: 'text/x-idl', mode: 'idl', ext: ['pro'] }, { name: 'Pug', mime: 'text/x-pug', mode: 'pug', ext: ['jade', 'pug'], alias: ['jade'] }, - { name: 'Java', mime: 'text/x-java', mode: 'clike', ext: ['java'] }, - { name: 'Java Server Pages', mime: 'application/x-jsp', mode: 'htmlembedded', ext: ['jsp'], alias: ['jsp'] }, + { name: 'Java', mime: 'text/x-java', mode: 'clike', ext: ['java', 'class'] }, + { name: 'Java Server Pages', mime: 'application/x-jsp', mode: 'application/x-jsp', ext: ['jsp'], alias: ['jsp'] }, { name: 'JavaScript', mimes: ['text/javascript', 'text/ecmascript', 'application/javascript', 'application/x-javascript', 'application/ecmascript'], mode: 'javascript', diff --git a/app/components/Editor/components/CodeEditor/mixins/basicMixin.jsx b/app/components/Editor/components/CodeEditor/mixins/basicMixin.jsx index 99cba62a..bdda8ec1 100644 --- a/app/components/Editor/components/CodeEditor/mixins/basicMixin.jsx +++ b/app/components/Editor/components/CodeEditor/mixins/basicMixin.jsx @@ -11,12 +11,12 @@ function changeInterceptor (editor) { let modFileContent = fileContent const { trimTrailingWhitespace, insertFinalNewline } = editor.options - if (trimTrailingWhitespace) { - modFileContent = modFileContent.replace(/[ \t]+$/gm, '') + if (trimTrailingWhitespace === 'on') { + modFileContent = modFileContent.replace(/[ \t]+$/gm, ''); } - if (insertFinalNewline) { - if (!modFileContent.endsWith('\n')) modFileContent += '\n' + if (insertFinalNewline === 'on') { + if (!modFileContent.endsWith('\n')) modFileContent += '\n'; } if (modFileContent !== fileContent) { diff --git a/app/components/Editor/components/CodeEditor/mixins/eslintMixin/eslintMixin.js b/app/components/Editor/components/CodeEditor/mixins/eslintMixin/eslintMixin.js index 804f1138..930cc1fc 100644 --- a/app/components/Editor/components/CodeEditor/mixins/eslintMixin/eslintMixin.js +++ b/app/components/Editor/components/CodeEditor/mixins/eslintMixin/eslintMixin.js @@ -2,7 +2,6 @@ import 'codemirror/addon/lint/lint.js' import 'codemirror/addon/lint/lint.css' import { extendObservable } from 'mobx' import linterFactory from './codemirror-eslint' -import { notify, NOTIFY_TYPE } from 'components/Notification/actions' function handleLinterError (error, cm) { cm.setOption('lint', { @@ -23,14 +22,19 @@ const lintOption = { toggle (cm) { this.enabled = !this.enabled cm && cm.performLint() - }, + } } export default { key: 'eslint', shouldMount () { const editor = this.editor - if (editor.modeInfo && (editor.modeInfo.mode === 'javascript' || editor.modeInfo.mode === 'jsx')) return true + if ( + editor.modeInfo && + (editor.modeInfo.mode === 'javascript' || editor.modeInfo.mode === 'jsx') + ) { + return true + } }, componentDidMount () { const cm = this.cm diff --git a/app/components/Editor/components/EditorWidgets/EditorWidgets.jsx b/app/components/Editor/components/EditorWidgets/EditorWidgets.jsx index 725b54ea..602128a5 100644 --- a/app/components/Editor/components/EditorWidgets/EditorWidgets.jsx +++ b/app/components/Editor/components/EditorWidgets/EditorWidgets.jsx @@ -1,5 +1,6 @@ import React, { Component } from 'react' import { observer, inject } from 'mobx-react' +import config from 'config' import ModeWidget from './ModeWidget' import LineWidget from './LineWidget' import LinterWidget from './LinterWidget' @@ -8,7 +9,7 @@ import EncodingWidget from './EncodingWidget' @inject(({ EditorTabState }) => { const activeTab = EditorTabState.activeTab if (!activeTab || !activeTab.editor) return { editor: null } - return { editor: activeTab.editor } + return { editor: activeTab.editorInfo } }) @observer class EditorWidgets extends Component { diff --git a/app/components/Editor/components/EditorWidgets/ModeWidget.jsx b/app/components/Editor/components/EditorWidgets/ModeWidget.jsx index 1d33b563..a56037d6 100644 --- a/app/components/Editor/components/EditorWidgets/ModeWidget.jsx +++ b/app/components/Editor/components/EditorWidgets/ModeWidget.jsx @@ -1,7 +1,10 @@ import React, { Component } from 'react' import { observer } from 'mobx-react' + +import config from 'config' import cx from 'classnames' import modeInfos from 'components/Editor/components/CodeEditor/addons/mode/modeInfos' +import monacoModeInfos from 'components/MonacoEditor/utils/modeInfos' import Menu from 'components/Menu' @observer @@ -23,11 +26,12 @@ export default class ModeWidget extends Component { } makeModeMenuItems () { - return modeInfos.map((mode) => ({ - key: mode.name, - name: mode.name, + const languageInfos = monaco.languages.getLanguages() + return languageInfos.map((mode) => ({ + key: mode.id, + name: mode.aliases[0] || mode.id, command: () => { - this.setMode(mode.name) + this.setMode(mode.id) }, })) } diff --git a/app/components/Editor/components/HtmlEditor/index.jsx b/app/components/Editor/components/HtmlEditor/index.jsx index 744c8146..187ad2af 100644 --- a/app/components/Editor/components/HtmlEditor/index.jsx +++ b/app/components/Editor/components/HtmlEditor/index.jsx @@ -73,7 +73,7 @@ class HtmlEditor extends Component { componentDidMount () { autorun(() => { - if (this.props.tab.file.isSynced) { + if (this.props.tab.file && this.props.tab.file.isSynced) { this.props.tab.previewUniqueId = uniqueId() } }) @@ -163,7 +163,7 @@ class HtmlEditor extends Component { } } -HtmlEditor.PropTypes = { +HtmlEditor.propTypes = { tab: PropTypes.object, } diff --git a/app/components/Editor/components/ImageEditor.jsx b/app/components/Editor/components/ImageEditor.jsx index f7dd2c43..aa6acc27 100644 --- a/app/components/Editor/components/ImageEditor.jsx +++ b/app/components/Editor/components/ImageEditor.jsx @@ -3,17 +3,20 @@ import PropTypes from 'prop-types' import config from 'config' import { request } from 'utils' -const previewPic = 'https://dn-coding-net-production-static.qbox.me/static/5d487aa5c207cf1ca5a36524acb953f1.gif' +const previewPic = 'https://coding-net-production-static.codehub.cn/static/5d487aa5c207cf1ca5a36524acb953f1.gif'; + class ImageEditor extends Component { constructor (props) { super(props) - this.state = {} + this.state = { + imageUrl: null, + background: null, + } } getImageUrl () { - const { baseURL, spaceKey } = config - const backgroundImageUrl = - `${baseURL}/workspaces/${spaceKey}/raw?path=${encodeURIComponent(this.props.path)}` + const { baseURL, spaceKey } = config; + const backgroundImageUrl = `${baseURL}/workspaces/${spaceKey}/raw?path=${encodeURIComponent(this.props.path)}`; request.get(backgroundImageUrl, {}, { responseType: 'blob', }).then(blob => { @@ -22,10 +25,46 @@ class ImageEditor extends Component { }) } + handleAlphaBackground() { + const imgElement = new Image(); + imgElement.src = this.state.imageUrl; + imgElement.crossOrigin = 'Anonymous'; + imgElement.onload = () => { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.width = 10; + canvas.height = 10; + // 清除画布 + context.clearRect(0, 0, 10, 10); + // 图片绘制在画布上 + context.drawImage(imgElement, 0, 0); + // 获取图片像素信息 + const imageData = context.getImageData(0, 0, 10, 10).data; + // 检测有没有透明数据 + let isAlphaBackground = false; + for (let i = 3; i < imageData.length; i += 4) { + if (imageData[i] != 255) { + isAlphaBackground = true; + break; + } + } + if (isAlphaBackground) { + this.setState({ background: `url("${previewPic}") right bottom #eee` }); + } + } + } + componentDidMount () { this.getImageUrl() } + componentDidUpdate() { + if (this.state.background) { + return; + } + this.handleAlphaBackground(); + } + componentWillReceiveProps ({ path }) { if (this.props.path !== path) { if (this.state.imageUrl) window.URL.revokeObjectURL(this.state.imageUrl) @@ -38,25 +77,18 @@ class ImageEditor extends Component { } render () { - if (!this.state.imageUrl) { + const { imageUrl, background } = this.state; + const img = background + ? preview + : preview; + if (!imageUrl) { return ( -
    - +
    +
    - ) + ); } - return ( -
    - preview -
    ) + return img; } } @@ -64,5 +96,4 @@ ImageEditor.propTypes = { path: PropTypes.string }; - -export default ImageEditor +export default ImageEditor; diff --git a/app/components/Editor/components/MarkdownEditor/index.jsx b/app/components/Editor/components/MarkdownEditor/index.jsx index 5ae4bd53..4e46d1c3 100644 --- a/app/components/Editor/components/MarkdownEditor/index.jsx +++ b/app/components/Editor/components/MarkdownEditor/index.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { autorun, extendObservable, observable, autorunAsync } from 'mobx' import debounce from 'lodash/debounce' import cx from 'classnames' -import marked from 'marked' import Remarkable from 'remarkable' import { observer } from 'mobx-react' import CodeEditor from '../CodeEditor' @@ -69,13 +68,6 @@ md.renderer.rules.heading_open = (tokens, idx) => { return '' } -marked.setOptions({ - highlight: (code) => { - require('highlight.js/styles/github-gist.css') - return require('highlight.js').highlightAuto(code).value - }, -}) - @observer class PreviewEditor extends Component { constructor (props) { @@ -142,7 +134,7 @@ class MarkdownEditor extends Component { showPreview: true, }) } - + this.state = observable({ previewContent: '', tokens: [] @@ -239,7 +231,7 @@ class MarkdownEditor extends Component { } } -MarkdownEditor.PropTypes = { +MarkdownEditor.propTypes = { tab: PropTypes.object, content: PropTypes.string, } diff --git a/app/components/Editor/components/MarkdownEditor/mdMixin.js b/app/components/Editor/components/MarkdownEditor/mdMixin.js index 46120075..cf1b7b98 100644 --- a/app/components/Editor/components/MarkdownEditor/mdMixin.js +++ b/app/components/Editor/components/MarkdownEditor/mdMixin.js @@ -1,7 +1,6 @@ import state from './state' // import findByTextContent from './utils' import debounce from 'lodash/debounce' -import marked from 'marked' import animatedScrollTo from 'animated-scrollto' const buildScrollMap = (previewDOM) => { diff --git a/app/components/Editor/components/UnknownEditor.jsx b/app/components/Editor/components/UnknownEditor.jsx index 00f253cd..00bca284 100644 --- a/app/components/Editor/components/UnknownEditor.jsx +++ b/app/components/Editor/components/UnknownEditor.jsx @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import filesize from 'filesize' import config from 'config' +import api from '../../../backendAPI' class UnknownEditor extends Component { constructor (props) { @@ -12,6 +13,7 @@ class UnknownEditor extends Component { } this.getFileUrl = this.getFileUrl.bind(this) this.getFileExt = this.getFileExt.bind(this) + this.handleDownload = this.handleDownload.bind(this) } getFileUrl () { @@ -28,11 +30,15 @@ class UnknownEditor extends Component { } } + handleDownload () { + api.downloadFile(this.props.path, false) + } + render () { return (
    -
    - +
    {`${this.props.path} - ${filesize(this.props.size)}`} diff --git a/app/components/Editor/state.js b/app/components/Editor/state.js index c17c71c6..1df4ced4 100644 --- a/app/components/Editor/state.js +++ b/app/components/Editor/state.js @@ -2,7 +2,7 @@ import uniqueId from 'lodash/uniqueId' import is from 'utils/is' import getTabType from 'utils/getTabType' import assignProps from 'utils/assignProps' -import { reaction, observe, observable, computed, action, autorun, extendObservable } from 'mobx' +import { observe, observable, computed, action, autorun, extendObservable } from 'mobx' import CodeMirror from 'codemirror' import FileStore from 'commons/File/store' import TabStore from 'components/Tab/store' @@ -10,15 +10,15 @@ import overrideDefaultOptions from './codemirrorDefaultOptions' import { loadMode } from './components/CodeEditor/addons/mode' import { findModeByFile, findModeByMIME, findModeByName } from './components/CodeEditor/addons/mode/findMode' +const defaultOptions = { ...CodeMirror.defaults, ...overrideDefaultOptions } + const typeDetect = (title, types) => { // title is the filename // typeArray is the suffix - if (!Array.isArray(types)) return title.endsWith(`.${types}`) - return types.reduce((p, v) => p || title.endsWith(`.${v}`), false) + if (!Array.isArray(types)) return title.toLowerCase().endsWith(`.${types}`) + return types.reduce((p, v) => p || title.toLowerCase().endsWith(`.${v}`), false) } -const defaultOptions = { ...CodeMirror.defaults, ...overrideDefaultOptions } - const state = observable({ entities: observable.map({}), options: observable.shallow(defaultOptions), @@ -34,6 +34,7 @@ state.entities.observe((change) => { class Editor { constructor (props = {}) { this.id = props.id || uniqueId('editor_') + this.contentType = props.contentType state.entities.set(this.id, this) this.update(props) if (!props.filePath || this.isCM) { @@ -65,34 +66,27 @@ class Editor { if (content !== cm.getValue()) cm.setValue(content) })) - this.disposers.push(reaction(() => { - if (this.tab && this.tab.isActive) return this.tab - }, (activeTab) => { - if (!activeTab) return - if (activeTab.editor && activeTab.editor.cm) { - setTimeout(() => { - activeTab.editor.cm.refresh() - activeTab.editor.cm.focus() - }, 1) - } - })) // 1. set value if (this.content) { cm.setValue(this.content) cm.clearHistory() - const scrollLine = this.scrollLine || 0 - if (scrollLine > 0) { - cm.scrollIntoView({ line: scrollLine, ch: 0 }) - // cm.setCursor({ line: scrollLine - 1, ch: 0 }) - } - cm.focus() } + // autorun(() => { + // const cursorLine = this.cursorLine || 0 + // if (cursorLine > 0) { + // cm.scrollIntoView({ line: cursorLine - 1, ch: 0 }) + // cm.setCursor({ line: cursorLine - 1, ch: 0 }) + // } + // }) + autorun(() => { - const cursorLine = this.cursorLine || 0 - if (cursorLine > 0) { - cm.scrollIntoView({ line: cursorLine - 1, ch: 0 }) - cm.setCursor({ line: cursorLine - 1, ch: 0 }) + const tab = this.tab; + if (tab && tab.isActive && tab.editor && tab.editor.cm) { + setTimeout(() => { + tab.editor.cm.refresh(); + tab.editor.cm.focus(); + }, 0); } }) @@ -201,29 +195,38 @@ class Editor { @computed get editorType () { - let type = 'default' - if (!this.file) return type - if (this.file.contentType) { - if (getTabType(this.file) === 'IMAGE') { - type = 'imageEditor' - } else if (getTabType(this.file) === 'UNKNOWN') { - type = 'unknownEditor' - } + const contentType = getTabType(this.contentType); + if (!this.file) { + return 'textEditor'; + } + if (typeDetect(this.file.name, ['md', 'markdown', 'mdown'])) { + return 'markdownEditor'; + } + if (typeDetect(this.file.name, ['html', 'htm'])) { + return 'htmlEditor' } - if (this.file.contentType === 'text/html') { - type = 'htmlEditor' - } else if (typeDetect(this.file.name, 'md')) { - type = 'editorWithPreview' + if (typeDetect(this.file.name, ['png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'bmp'])) { + return 'imageEditor'; } - if (typeDetect(this.file.name, ['png', 'jpg', 'jpeg', 'gif'])) { - type = 'imageEditor' + switch (contentType) { + case 'TEXT': + return 'textEditor'; + case 'HTML': + return 'htmlEditor'; + case 'MARKDOWN': + return 'markdownEditor'; + case 'IMAGE': + return 'imageEditor'; + case 'UNKNOWN': + return 'unknownEditor'; + default: + return 'unknownEditor'; } - return type } @computed get isCM () { - return this.editorType === 'default' || this.editorType === 'editorWithPreview' || this.editorType === 'htmlEditor' + return ['textEditor', 'markdownEditor', 'htmlEditor'].includes(this.editorType); } disposers = [] diff --git a/app/components/FileTree/actions.js b/app/components/FileTree/actions.js index 36db4868..6b0605b5 100644 --- a/app/components/FileTree/actions.js +++ b/app/components/FileTree/actions.js @@ -2,18 +2,16 @@ import _ from 'lodash' import api from 'backendAPI' import { registerAction } from 'utils/actions' import FileStore from 'commons/File/store' -import * as TabActions from 'components/Tab/actions' import * as Modal from 'components/Modal/actions' import contextMenuStore from 'components/ContextMenu/store' import state, { FileTreeNode } from './state' -import bindToFile from './fileTreeToFileBinding' +import bindToFile, { isFileExcluded } from './fileTreeToFileBinding' import FileTreeContextMenuItems from './contextMenuItems' import dispatchCommand from 'commands/dispatchCommand' -import { getTabType } from 'utils' import i18n from 'utils/createI18n' -import icons from 'file-icons-js' +import is from 'utils/is' import statusBarState from '../StatusBar/state' -import { notify, NOTIFY_TYPE } from '../Notification/actions' +import notification from '../Notification' const MAX_FILE_SIZE_MB = 10 const MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 @@ -24,13 +22,13 @@ export const initializeFileTree = registerAction('filetree:init', () => { bindToFile(state, FileState, FileTreeNode) }) -export const selectNode = registerAction('filetree:select_node', +export const selectNode = registerAction( + 'filetree:select_node', (node, multiSelect) => ({ node, multiSelect }), ({ node, multiSelect }) => { const offset = node if (typeof offset === 'number') { node = undefined - if (offset === 1) { const curNode = state.focusedNodes[state.focusedNodes.length - 1] if (curNode) node = curNode.getNext @@ -38,28 +36,28 @@ export const selectNode = registerAction('filetree:select_node', const curNode = state.focusedNodes[0] if (curNode) node = curNode.getPrev } - if (!node || node.isShadowRoot) node = state.root } - if (!multiSelect) { state.root.unfocus() state.shadowRoot.forEachDescendant(childNode => childNode.unfocus()) } - node.focus() } ) -export const highlightDirNode = registerAction('filetree:highlight_dir_node', +export const highlightDirNode = registerAction( + 'filetree:highlight_dir_node', node => node.isDir && node.highlight() ) -export const unhighlightDirNode = registerAction('filetree:unhighlight_dir_node', +export const unhighlightDirNode = registerAction( + 'filetree:unhighlight_dir_node', node => node.isDir && node.unhighlight() ) -export const toggleNodeFold = registerAction('filetree:toggle_node_fold', +export const toggleNodeFold = registerAction( + 'filetree:toggle_node_fold', (node, shouldBeFolded, deep) => ({ node, shouldBeFolded, deep }), ({ node, shouldBeFolded = null, deep = false }) => { if (!node.isDir) return @@ -80,6 +78,43 @@ export const removeNode = registerAction('filetree:remove_node', (node) => { export const openContextMenu = contextMenuStore.openContextMenuFactory(FileTreeContextMenuItems) export const closeContextMenu = contextMenuStore.closeContextMenu +export const syncDirectory = registerAction('filetree:sync_file', (node, deep = false) => { + if (node.isDir) { + node.isLoaded = false + node.isLoading = true + FileStore.fetchPath(node.path) + .then((data) => { + if (deep) { + data.forEach((d) => { + if ( + d.isDir && + !isFileExcluded(d.path) && + (d.filesCount > 0 || d.directoriesCount > 0) + ) { + const fileNode = state.entities.get(d.path) + fileNode && fileNode.isLoaded && syncDirectory(fileNode, true) + } + }) + } + FileStore.loadNodeData(data) + }) + .then((res) => { + node.isLoading = false + node.isLoaded = true + }) + } +}) + +export const syncAllDirectoryByPath = registerAction('filetree:sync_all_dir', (rootPath) => { + if (!is.string(rootPath)) { + return false + } + const rootNode = state.entities.get(rootPath) + if (rootNode.isDir) { + syncDirectory(rootNode, true) + } +}) + const openNodeCommonLogic = function (node, editor, shouldBeFolded = null, deep = false) { if (node.isDir) { if (!node.isLoaded) { @@ -94,20 +129,13 @@ const openNodeCommonLogic = function (node, editor, shouldBeFolded = null, deep } else { toggleNodeFold(node, shouldBeFolded, deep) } - } else if (getTabType(node) === 'TEXT') { - dispatchCommand('file:open_file', { path: node.path, editor }) } else { - TabActions.createTab({ - title: node.name, - icon: icons.getClassWithColor(node.name) || 'fa fa-file-text-o', - editor: { - ...editor, - filePath: node.path, - }, - }) + dispatchCommand('file:open_file', { path: node.path, editor, contentType: node.contentType }) } } -export const openNode = registerAction('filetree:open_node', + +export const openNode = registerAction( + 'filetree:open_node', (node, shouldBeFolded, deep) => ({ node, shouldBeFolded, deep }), ({ node, shouldBeFolded = null, deep = false }) => { openNodeCommonLogic(node, {}, shouldBeFolded, deep) @@ -123,32 +151,33 @@ export const gitBlameNode = registerAction('filetree:git_blame', (node) => { export const uploadFilesToPath = (files, path) => { if (!files.length) return const node = state.entities.get(path) - const targetDirPath = node.isDir ? node.path : (node.parent.path || '/') + const targetDirPath = node.isDir ? node.path : node.parent.path || '/' _(files).forEach((file) => { if (file.size > MAX_FILE_SIZE) { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: i18n`file.fileToLarge${{ filesize: MAX_FILE_SIZE_MB }}` + notification.error({ + description: i18n`file.fileToLarge${{ filesize: MAX_FILE_SIZE_MB }}` }) return } api.uploadFile(targetDirPath, file, { onUploadProgress: (progressEvent) => { const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total) - statusBarState.setFileUploadInfo({ path: file.name, info: { percentCompleted, size: file.size } }) + statusBarState.setFileUploadInfo({ + path: file.name, + info: { percentCompleted, size: file.size } + }) }, onUploadFailed: () => { statusBarState.removeFileUploadInfo({ path: file.name }) - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: i18n.get('file.uploadFailed') + `: ${file.name}`, + notification.error({ + description: `${i18n.get('file.uploadFailed')}: ${file.name}` }) } }) }) } -export const mv = (from, to, force=false) => { +export const mv = (from, to, force = false) => { const name = from.split('/').pop() const newPath = `${to}/${name}` if (from === newPath) return @@ -167,7 +196,7 @@ export const mv = (from, to, force=false) => { } else if (/directory.*exist/.test(res.msg)) { await Modal.showModal('Alert', { header: i18n`file.moveFolderFailed`, - message: i18n`file.folderExist`, + message: i18n`file.folderExist` }) Modal.dismissModal() } diff --git a/app/components/FileTree/contextMenuItems.js b/app/components/FileTree/contextMenuItems.js index 252321d9..ab19c5cb 100644 --- a/app/components/FileTree/contextMenuItems.js +++ b/app/components/FileTree/contextMenuItems.js @@ -1,4 +1,4 @@ -import { gitBlameNode } from './actions' +import { gitBlameNode, syncDirectory, isInVCS } from './actions' import i18n from 'utils/createI18n' const divider = { isDivider: true } @@ -16,16 +16,21 @@ const items = [ command: 'file:new_folder', }, - divider, + { + isDivider: true, + getIsHidden: ctx => !ctx.id + }, { name: i18n`fileTree.contextMenu.delete`, icon: 'fa fa-trash-o', command: 'file:delete', id: 'filetree_menu_delete', + getIsHidden: ctx => !ctx.id }, { name: i18n`fileTree.contextMenu.rename`, icon: 'fa', command: 'file:rename', + getIsHidden: ctx => !ctx.id }, divider, { @@ -45,14 +50,20 @@ const items = [ getIsHidden: ctx => !ctx.isDir, }, - divider, { - name: i18n`fileTree.contextMenu.gitBlame`, - icon: 'fa', + name: i18n`fileTree.contextMenu.sync`, + icon: 'fa fa-refresh', command: (c) => { - gitBlameNode(c) + syncDirectory(c) }, - id: 'filetree_menu_gitBlame', + getIsHidden: ctx => !ctx.isDir, + }, + divider, + { + name: i18n`fileTree.contextMenu.ignore`, + icon: 'fa', + command: 'file:add_ignore', + id: 'filetree_menu_ignore' } ] diff --git a/app/components/FileTree/fileTreeToFileBinding.js b/app/components/FileTree/fileTreeToFileBinding.js index 93568026..1cfbad88 100644 --- a/app/components/FileTree/fileTreeToFileBinding.js +++ b/app/components/FileTree/fileTreeToFileBinding.js @@ -3,7 +3,7 @@ import minimatch from 'minimatch' import { reaction } from 'mobx' import is from 'utils/is' -function isFileExcluded (filePath) { +export function isFileExcluded (filePath) { if (filePath === '') return false return config.fileExcludePatterns.reduce((isMatched, pattern) => { if (isMatched) return true diff --git a/app/components/FileTree/state.js b/app/components/FileTree/state.js index 3e16eda1..7d5f2c2a 100644 --- a/app/components/FileTree/state.js +++ b/app/components/FileTree/state.js @@ -117,5 +117,7 @@ class FileTreeNode extends TreeNode { /* end extend */ } +const fileIconProviders = observable.map() + export default state -export { FileTreeNode, TreeNode } +export { FileTreeNode, TreeNode, fileIconProviders } diff --git a/app/components/FileTreeToolBar/FileTreeToolBar.jsx b/app/components/FileTreeToolBar/FileTreeToolBar.jsx new file mode 100644 index 00000000..f643a626 --- /dev/null +++ b/app/components/FileTreeToolBar/FileTreeToolBar.jsx @@ -0,0 +1,61 @@ +import React, { Component } from 'react' + +import { toggleNodeFold, syncAllDirectoryByPath } from 'components/FileTree/actions' +import { syncFile } from 'commons/File/actions' +import FileTreeState from 'components/FileTree/state' +import i18n from 'utils/createI18n' + +class FileTreeToolBar extends Component { + constructor(props) { + super(props); + this.state = { + syncActive: false, + collapseActive: false, + }; + } + + syncTree = () => { + if (this.state.syncActive === true) { + return; + } + this.setState({ syncActive: true }); + this.syncTimer = setTimeout(() => { + this.setState({ syncActive: false }); + }, 400); + syncAllDirectoryByPath(''); + } + + foldedAll = () => { + if (this.state.collapseActive === true) { + return; + } + const { children } = FileTreeState.root; + if (!children || children.length === 0) { + return; + } + this.setState({ collapseActive: true }); + this.collapseTimer = setTimeout(() => { + this.setState({ collapseActive: false }); + }, 200); + children.forEach(child => toggleNodeFold(child, true, true)); + } + + componentWillUnmount() { + clearTimeout(this.syncTimer); + clearTimeout(this.collapseTimer); + } + + render () { + return ( +
    + + + + + +
    + ) + } +} + +export default FileTreeToolBar; diff --git a/app/components/FileTreeToolBar/index.js b/app/components/FileTreeToolBar/index.js new file mode 100644 index 00000000..1d8986b0 --- /dev/null +++ b/app/components/FileTreeToolBar/index.js @@ -0,0 +1,3 @@ +import FileTreeToolBar from './FileTreeToolBar' + +export default FileTreeToolBar diff --git a/app/components/Git/GitBranchWidget.jsx b/app/components/Git/GitBranchWidget.jsx index 02d60767..52826c31 100644 --- a/app/components/Git/GitBranchWidget.jsx +++ b/app/components/Git/GitBranchWidget.jsx @@ -7,7 +7,7 @@ import { connect } from 'react-redux' import * as GitActions from './actions' import Menu from '../Menu' import i18n from 'utils/createI18n' - +import config from 'config' // add withRef to deliver ref to the wrapperedcomponent @connect(state => state.GitState.branches, @@ -28,7 +28,7 @@ export default class GitBranchWidget extends Component { const { current: currentBranch, local: localBranches, remote: remoteBranches } = this.props return (
    { this.toggleActive(true, true) }} + onClick={e => { this.toggleActive(true, true); config.menuBars.push(this) }} > @@ -63,6 +63,7 @@ export default class GitBranchWidget extends Component { return [{ name: i18n.get('git.branchWidget.fetchingBranches'), isDisabled: true }] } + const { current } = this.props const localBranchItems = localBranches.map(branch => ({ name: branch, icon: 'fa', @@ -99,10 +100,18 @@ export default class GitBranchWidget extends Component { } }) return [ - { name: i18n.get('git.branchWidget.newBranch'), command: () => dispatchCommand('git:new_branch'), - iconElement: (+) }, - { name: i18n.get('git.branchWidget.synchronize'), command: () => this.props.getFetch(), - icon: 'fa' }, + { + name: i18n.get('git.branchWidget.newBranch'), + command: () => dispatchCommand('git:new_branch'), + iconElement: (+), + isDisabled: current === '' || current === undefined + }, + { + name: i18n.get('git.branchWidget.synchronize'), + command: () => this.props.getFetch(), + icon: 'fa', + isDisabled: current === '' || current === undefined + }, { isDivider: true }, { name: i18n.get('git.branchWidget.localBranches'), isDisabled: true }, ...localBranchItems, diff --git a/app/components/Git/GitCommitView.jsx b/app/components/Git/GitCommitView.jsx index cac35346..c54873bc 100644 --- a/app/components/Git/GitCommitView.jsx +++ b/app/components/Git/GitCommitView.jsx @@ -18,11 +18,7 @@ var GitCommitView = ({isWorkingDirClean, ...actionProps}) => { : (
    { - diffFile({ - path, newRef: 'HEAD', oldRef: '~~unstaged~~' - }) - }} + handleClick={path => diffFile({ path, newRef: 'HEAD', oldRef: '~~unstaged~~' })} />
    diff --git a/app/components/Git/GitFileTree.jsx b/app/components/Git/GitFileTree.jsx index d9969e3e..1a337996 100644 --- a/app/components/Git/GitFileTree.jsx +++ b/app/components/Git/GitFileTree.jsx @@ -59,7 +59,7 @@ class _GitFileTreeNode extends Component { return (
    { node.isRoot ? - (
    this.nodeDOM = r} > + (
    this.nodeDOM = r}> { displayOnly ? null : { let ref @@ -29,39 +34,58 @@ const RefTag = ({ value: refName, backgroundColor, borderColor }) => { if (!ref) return null return ( -
    - {ref.name} +
    + + {ref.name}
    ) } -@inject(() => { - return { - commitsState: state.commitsState, - commits: Array.from(state.commitsState.commits.values()), - } -}) +@inject(() => ({ + commitsState: state.commitsState, + commits: Array.from(state.commitsState.commits.values()) +})) @observer +@connect(reduxState => ({ currentBranch: reduxState.GitState.branches.current }), + dispatch => bindActionCreators({ getBranches }, dispatch), + null, + { + withRef: true + } +) class GitGraphTable extends Component { constructor (props) { super(props) fetchRefs() const PAGE_SIZE = 30 + this.onShow = this.onShow.bind(this) fetchCommits({ size: PAGE_SIZE, page: 0 }) this.state = { radius: 4, colWidth: 10, rowHeight: 25, selectedRowIndex: -1, - viewSize: 'small', + viewSize: 'small' } this.crawler = new CommitsCrawler({ commits: state.rawCommits, - size: PAGE_SIZE, + size: PAGE_SIZE }) } + componentDidMount () { + emitter.on(E.GITGRAPH_SHOW, this.onShow) + } + + componentWillUnmount () { + emitter.removeListener(E.GITGRAPH_SHOW, this.onShow) + } + + onShow () { + this.props.getBranches() + } + toggleViewSize = (smallOrLarge) => { if (!smallOrLarge) smallOrLarge = this.state.viewSize === 'small' ? 'large' : 'small' switch (smallOrLarge) { @@ -85,91 +109,118 @@ class GitGraphTable extends Component { render () { const { radius, colWidth, rowHeight } = this.state - const { commits, commitsState } = this.props + const { commits, commitsState, currentBranch } = this.props return ( -
    this.wrapperDOM = r} +
    (this.wrapperDOM = r)} onScroll={this.onVerticalScroll} > -
    -
    - -
    + {currentBranch === '' || currentBranch === undefined ? ( +

    {i18n`git.initPrompt`}

    + ) : ( +
    +
    + +
    - {commits.map((commit, commitIndex) => - this.state.viewSize === 'large' ? - (
    this.setState({ selectedRowIndex: commitIndex })} - > -
    - -
    -
    -
    -
    `}> - {commit.author.name} -
    - {commit.refs.map(ref => - + this.state.viewSize === 'large' ? ( +
    this.setState({ selectedRowIndex: commitIndex })} + > +
    + - )} -
    - {moment(commit.date, 'X').format('YYYY/MM/DD')} +
    +
    +
    +
    `} + > + {commit.author.name} +
    + {commit.refs.map(ref => ( + + ))} +
    + {moment(commit.date, 'X').format('YYYY/MM/DD')} +
    +
    +
    +
    {commit.shortId}
    +
    {commit.message}
    +
    -
    + ) : ( +
    this.setState({ selectedRowIndex: commitIndex })} + >
    {commit.shortId}
    -
    {commit.message}
    +
    + {commit.refs.map(ref => ( + + ))} +
    {commit.message}
    +
    +
    `} + > + {commit.author.name} +
    +
    + {moment(commit.date, 'X').format('MM/DD/YYYY')} +
    -
    -
    ) - - : (
    this.setState({ selectedRowIndex: commitIndex })} - > -
    {commit.shortId}
    -
    - {commit.refs.map(ref => - - )} -
    {commit.message}
    -
    -
    `}> - {commit.author.name} -
    -
    - {moment(commit.date, 'X').format('MM/DD/YYYY')} -
    -
    - ) - )} -
    + ) + )} +
    + )}
    ) } diff --git a/app/components/Git/actions.js b/app/components/Git/actions.js index 14d87dfb..051afbae 100644 --- a/app/components/Git/actions.js +++ b/app/components/Git/actions.js @@ -1,11 +1,12 @@ import _ from 'lodash' import api from '../../backendAPI' -import { notify, NOTIFY_TYPE } from '../Notification/actions' +import notification from '../Notification' import { showModal, addModal, dismissModal, updateModal } from '../Modal/actions' import { createAction } from 'redux-actions' import Clipboard from 'clipboard' import i18n from 'utils/createI18n' +import statusBarState from '../StatusBar/state' export const GIT_STATUS = 'GIT_STATUS' export const updateStatus = createAction(GIT_STATUS) @@ -14,19 +15,30 @@ export const GIT_UPDATE_COMMIT_MESSAGE = 'GIT_UPDATE_COMMIT_MESSAGE' export const updateCommitMessage = createAction(GIT_UPDATE_COMMIT_MESSAGE) export function commit () { + statusBarState.displayBar = true return (dispatch, getState) => { const GitState = getState().GitState const stagedFiles = GitState.statusFiles.filter(file => file.isStaged) const stagedFilesPathList = stagedFiles.toArray().map(stagedFile => stagedFile.path.replace(/^\//, '')) const initialCommitMessage = i18n.get('git.commitView.initMessage') - console.log(GitState.commitMessage); return api.gitCommit({ files: stagedFilesPathList, message: GitState.commitMessage || initialCommitMessage, }).then((filetreeDelta) => { + statusBarState.displayBar = false + if (filetreeDelta.code) { + notification.error({ + description: filetreeDelta.msg + }) + return; + } dispatch(updateCommitMessage('')) - notify({ message: i18n`git.action.commitSuccess` }) + notification.success({ + description: i18n`git.action.commitSuccess` + }) dismissModal() + }).catch(err => { + statusBarState.displayBar = false }) } } @@ -40,7 +52,9 @@ export function updateStagingArea (action, file) { export const GIT_FETCH = 'GIT_FETCH' export function getFetch () { return dispatch => api.gitFetch().then( - notify({ message: i18n`git.action.fetchSuccess` }) + notification.success({ + description: i18n`git.action.fetchSuccess` + }) ) } @@ -61,12 +75,13 @@ export function checkoutBranch (branch, remoteBranch) { if (data.status === 'OK') { // 完全由 ws 里的 checkout 事件来改变显示 // dispatch(createAction(GIT_CHECKOUT)({ branch })) - notify({ message: i18n`git.action.checkoutBranch${branch}` }) + notification.success({ + description: i18n`git.action.checkoutBranch${branch}` + }) } else if (data.status === 'CONFLICTS') { dispatch(createAction(GIT_CHECKOUT_FAILED)({ branch })) - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: i18n`git.action.checkoutConflictsWarning`, + notification.error({ + description: i18n`git.action.checkoutConflictsWarning`, }) api.gitStatus().then(({ files, clean }) => { @@ -92,9 +107,8 @@ export function checkoutBranch (branch, remoteBranch) { // dispatch(updateStatus({files, isClean: false})) // showModal('GitResolveConflicts') } else if (data.status === 'NONDELETED') { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: i18n`git.action.checkoutFailedWithoutDeleted`, + notification.error({ + description:i18n`git.action.checkoutFailedWithoutDeleted`, }) const files = [] data.undeletedList.map((file) => { @@ -103,9 +117,8 @@ export function checkoutBranch (branch, remoteBranch) { dispatch(updateStatus({ files, isClean: false })) showModal({ type: 'GitResolveConflicts', title: i18n`git.action.checkoutNotDeleted`, disableClick: true }) } else { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: i18n`An Exception occurred during checkout, status: ${{ status: data.status }}`, + notification.error({ + description:i18n`An Exception occurred during checkout, status: ${{ status: data.status }}`, }) } }) @@ -116,7 +129,9 @@ export const GIT_DELETE_BRANCH = 'GIT_DELETE_BRANCH' export function gitDeleteBranch (branch) { return (dispatch) => { api.gitDeleteBranch(branch).then(() => { - notify({ message: i18n`git.action.deletedSuccess${branch}` }) + notification.success({ + description:i18n`git.action.deletedSuccess${branch}` + }) }) } } @@ -129,21 +144,79 @@ export function getTags () { } export function pull () { + statusBarState.displayBar = true return (dispatch) => { api.gitPull().then((res) => { - notify({ message: 'Git pull success.' }) + statusBarState.displayBar = false + notification.success({ + description:'Git pull success.' + }) }).catch((res) => { - notify({ message: `Git pull fail: ${res.response.data.msg}` }) + statusBarState.displayBar = false + notification.error({ + description:`Git pull fail: ${res.response.data.msg}` + }) }) } } export function push () { + statusBarState.displayBar = true return (dispatch) => { api.gitPushAll().then((res) => { - notify({ message: 'Git push success.' }) + statusBarState.displayBar = false + if (res.nothingToPush) { + notification.info({ + description: 'Git push fail: nothing to push.' + }) + return; + } + if (res.ok) { + notification.success({ + description: 'Git push success.' + }) + } else { + if(res.updates.length===1){ + switch(res.updates[0].status) { + case 'UP_TO_DATE': + notification.info({ + message:'Git push: UP_TO_DATE.', + description: `${localRefName} -> ${remoteRefName}` + }) + break + case 'OK': + notification.success({ + message:'Git push: SUCCESS.', + description: `${localRefName} -> ${remoteRefName}` + }) + break + default: + notification.error({ + message:'Git push: FAIL.', + description: `${localRefName} -> ${remoteRefName}` + }) + break + } + }else{ + let failNum=0,successNum=0 + res.updates.map(v=>{ + if(v.status==="OK"||v.status==="UP_TO_DATE"){ + successNum += 1 + }else{ + failNum += 1 + } + }) + notification.info({ + message:'Git push: ', + description: ` ${failNum} failed, ${successNum} succeeded.`, + }) + } + } }).catch((res) => { - notify({ message: `Git push fail: ${res.response.data.msg}` }) + statusBarState.displayBar = false + notification.error({ + description: `Git push fail: ${res.response.data.msg}` + }) }) } } @@ -156,7 +229,9 @@ export const updateStashMessage = createAction(GIT_UPDATE_STASH_MESSAGE) export function createStash (message) { return (dispatch, getState) => api.gitCreateStash(message).then((res) => { - notify({ message: 'Git stash success.' }) + notification.success({ + description: 'Git stash success.' + }) dismissModal() const GitState = getState().GitState if (GitState.branches.failed) { @@ -164,9 +239,8 @@ export function createStash (message) { dispatch(createAction(GIT_CHECKOUT_FAILED)({ branch: '' })) } }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: err.msg, + notification.error({ + description: err.msg, }) dismissModal() }) @@ -203,36 +277,39 @@ export const selectStash = createAction(GIT_SELECT_STASH) export function dropStash (stashRef, all) { return dispatch => api.gitDropStash(stashRef, all).then((res) => { - notify({ message: 'Drop stash success.' }) + notification.success({ + description: 'Drop stash success.' + }) getStashList()(dispatch) }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Drop stash error.', + notification.error({ + description: 'Drop stash error.', }) }) } export function applyStash ({ stashRef, pop, applyIndex }) { return dispatch => api.gitApplyStash({ stashRef, pop, applyIndex }).then((res) => { - notify({ message: 'Apply stash success.' }) + notification.success({ + description: 'Apply stash success.' + }) dismissModal() }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Apply stash error.', + notification.error({ + description: 'Apply stash error.', }) }) } export function checkoutStash ({ stashRef, branch }) { return dispatch => api.gitCheckoutStash({ stashRef, branch }).then((res) => { - notify({ message: 'Checkout stash success.' }) + notification.success({ + description: 'Checkout stash success.' + }) dismissModal() }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Checkout stash error.', + notification.error({ + description: 'Checkout stash error.', }) }) } @@ -240,47 +317,50 @@ export function checkoutStash ({ stashRef, branch }) { export function getCurrentBranch (showSuccess) { return dispatch => api.gitCurrentBranch().then(({ name }) => { dispatch(updateCurrentBranch({ name })) - if (showSuccess) notify({ message: 'sync success' }) + if (showSuccess) notification.success({description: 'sync success' }) }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Get current branch error.', - }) + notification.error({description: 'Get current branch error.' }) }) } export function resetHead ({ ref, resetType }) { return dispatch => api.gitResetHead({ ref, resetType }).then((res) => { - notify({ message: 'Reset success.' }) + notification.success({description: 'Reset success.' }) dismissModal() }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Reset error.', - }) + notification.error({description: 'Reset error.' }) }) } export function addTag ({ tagName, ref, message, force }) { return dispatch => api.gitAddTag({ tagName, ref, message, force }).then((res) => { - notify({ message: 'Add tag success.' }) + notification.success({description: 'Add tag success.' }) dismissModal() }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Add tag error: ${err.msg}`, - }) + notification.error({description: `Add tag error: ${err.msg}` }) }) } export function mergeBranch (branch) { return dispatch => api.gitMerge(branch).then((res) => { // Merge conflict 的情况也是 200 OK,但 sucecss:false - if (!res.success) return res - notify({ message: 'Merge success.' }) - dismissModal() + if (!res.success) { + return res; + } else { + if (res.status === 'ALREADY_UP_TO_DATE') { + notification.info({description: 'Nothing to merge, already up to date.' }) + } else { + notification.success({description: 'Merge success.' }) + } + dismissModal(); + } }).then((res) => { - if (res.status && res.status === 'CONFLICTING') { + if (!res) { + return; + } + if (res.status === 'FAILED') { + notification.error({description: 'Merge failed.' }) + } else if (res.status === 'CONFLICTING') { dismissModal() api.gitStatus().then(({ files, clean }) => { files = _.filter(files, file => file.status == 'CONFLICTION') @@ -290,22 +370,16 @@ export function mergeBranch (branch) { ) } }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Merge error: ${err.msg}`, - }) + notification.error({description: `Merge error: ${err.msg}` }) }) } export function newBranch (branch) { return dispatch => api.gitNewBranch(branch).then((res) => { - notify({ message: 'Create new branch success.' }) + notification.success({description: 'Create new branch success.' }) dismissModal() }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Create new branch error: ${err.msg}`, - }) + notification.error({description: `Create new branch error: ${err.msg}`}) }) } @@ -323,6 +397,9 @@ export const selectNode = createAction(GIT_STATUS_SELECT_NODE, node => node) export const GIT_STATUS_STAGE_NODE = 'GIT_STATUS_STAGE_NODE' export const toggleStaging = createAction(GIT_STATUS_STAGE_NODE, node => node) +export const GIT_STATUS_STAGE_ALL = 'GIT_STATUS_STAGE_ALL'; +export const toggleStagingAll = createAction(GIT_STATUS_STAGE_ALL); + export const GIT_MERGE = 'GIT_MERGE' export const gitMerge = createAction(GIT_MERGE) export function mergeFile (path) { @@ -353,16 +430,11 @@ export function readFile ({ path }) { export function resolveConflict ({ path, content }) { return dispatch => api.gitResolveConflict({ path, content }).then((res) => { - notify({ - message: 'Resolve conflict success.', - }) + notification.success({description: 'Resolve conflict success.'}) dismissModal() updateModal({ isInvalid: true }) }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Resolve conflict error.', - }) + notification.error({description: 'Resolve conflict error.'}) }) } @@ -370,36 +442,27 @@ export function cancelConflict ({ path }) { return dispatch => api.gitCancelConflict({ path }).then((res) => { dismissModal() }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Cancel conflict error: ${err.msg}`, - }) + notification.error({description: `Cancel conflict error: ${err.msg}`}) }) } export function rebase ({ branch, upstream, interactive, preserve }) { return dispatch => api.gitRebase({ branch, upstream, interactive, preserve }).then((res) => { if (res.success) { - notify({ message: 'Rebase success.' }) + notification.success({description: 'Rebase success.'}) dismissModal() } else { dismissModal() resolveRebase(res, dispatch) } }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Rebase error: ${err.msg}`, - }) + notification.error({description: `Rebase error: ${err.msg}`}) }) } function resolveRebase (data, dispatch) { if (data.status === 'STOPPED') { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: 'Rebase STOPPED', - }) + notification.error({description: 'Rebase STOPPED'}) api.gitStatus().then(({ files, clean }) => { dispatch(updateStatus({ files, isClean: clean })) }).then(() => @@ -408,20 +471,14 @@ function resolveRebase (data, dispatch) { } else if (data.status === 'INTERACTIVE_EDIT') { showModal('GitRebaseInput', data.message) } else if (data.status === 'ABORTED') { - notify({ - message: 'Rebase aborted.', - }) + notification.info({description: 'Rebase aborted.'}) } else if (data.status === 'INTERACTIVE_PREPARED') { const rebaseTodoLines = data.rebaseTodoLines showModal('GitRebasePrepare', rebaseTodoLines) } else if (data.status === 'UNCOMMITTED_CHANGES') { - notify({ - message: 'Cannot rebase: Your index contains uncommitted changes. Please commit or stash them.', - }) + notification.info({description: 'Cannot rebase: Your index contains uncommitted changes. Please commit or stash them.'}) } else if (data.status === 'EDIT') { - notify({ - message: 'Current status is EDIT, we have stopped rebasing for you. \nPlease edit your files and then continue rebasing.', - }) + notification.info({description: 'Current status is EDIT, we have stopped rebasing for you. Please edit your files and then continue rebasing.'}) } } @@ -448,36 +505,26 @@ export function getRebaseState () { export function gitRebaseOperate ({ operation, message }) { return dispatch => api.gitRebaseOperate({ operation, message }).then((res) => { if (res.success) { - notify({ - message: 'Operate success.', - }) + notification.success({description: 'Operate success.'}) } else { resolveRebase(res, dispatch) } }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Operate error: ${err.msg}`, - }) + notification.error({description: `Operate error: ${err.msg}`}) }) } export function gitRebaseUpdate (lines) { return dispatch => api.gitRebaseUpdate(lines).then((res) => { if (res.success) { - notify({ - message: 'Rebase success.', - }) + notification.success({description: 'Rebase success.'}) dismissModal() } else { dismissModal() resolveRebase(res, dispatch) } }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Rebase error: ${err.msg}`, - }) + notification.error({description: `Rebase error: ${err.msg}`}) }) } @@ -496,10 +543,7 @@ export function gitCommitDiff ({ rev, title, oldRef }) { dispatch(updateCommitDiff({ files, ref: rev, title, oldRef })) addModal('GitCommitDiff') }).catch((err) => { - notify({ - notifyType: NOTIFY_TYPE.ERROR, - message: `Get commit diff error: ${err.msg}`, - }) + notification.error({description: `Get commit diff error: ${err.msg}`}) }) } diff --git a/app/components/Git/modals/merge.jsx b/app/components/Git/modals/merge.jsx index 9953e7df..029690bb 100644 --- a/app/components/Git/modals/merge.jsx +++ b/app/components/Git/modals/merge.jsx @@ -36,7 +36,7 @@ class GitMergeView extends Component { value={this.state.branchToMerge} style={this.state.selectChanged ? null : { color: '#aaa' }} > - {allBranches.map(branch => )} diff --git a/app/components/Git/modals/mergeFile.jsx b/app/components/Git/modals/mergeFile.jsx index f75aa3c3..ee8e8ce9 100644 --- a/app/components/Git/modals/mergeFile.jsx +++ b/app/components/Git/modals/mergeFile.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import { bindActionCreators } from 'redux' import { dispatchCommand } from '../../../commands' import cx from 'classnames' +import { inject } from 'mobx-react' import { connect } from 'react-redux' import * as GitActions from '../actions' import i18n from 'utils/createI18n' @@ -16,6 +17,9 @@ require(['diff_match_patch'], (lib) => { }) import 'codemirror/addon/merge/merge.css' +@inject(state => ({ + themeName: state.SettingState.settings.appearance.syntax_theme.value, +})) class GitMergeView extends Component { static defaultProps = { mode: null, @@ -95,6 +99,7 @@ class GitMergeView extends Component { origLeft: data.local, orig: data.remote, value: data.base, + theme: this.props.themeName, revertButtons: true, }) this.mergeView.wrap.style.height = '100%' diff --git a/app/components/Git/modals/tag.jsx b/app/components/Git/modals/tag.jsx index ff65c0bc..45fcedba 100644 --- a/app/components/Git/modals/tag.jsx +++ b/app/components/Git/modals/tag.jsx @@ -31,27 +31,25 @@ class GitTagView extends Component {

    {i18n`git.tag.title`}


    -
    - - +
    + +
    -
    - - +
    + + {if (e.keyCode === 13) { + e.preventDefault() + this.addTag() + }}} + />
    -
    - -