From d2edf7feb2bf17ae05319b672522d97af2baac56 Mon Sep 17 00:00:00 2001 From: hackape Date: Thu, 17 Aug 2017 17:53:47 +0800 Subject: [PATCH 1/6] ask before window close to prevent accidental close --- app/config.js | 1 + app/initialize/state.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/app/config.js b/app/config.js index d84e2b38..8ea52817 100644 --- a/app/config.js +++ b/app/config.js @@ -14,6 +14,7 @@ const config = observable({ fsSocketConnected: false, ttySocketConnected: false, fileExcludePatterns: ['/.git', '/.coding-ide'], + preventAccidentalClose: false, }) window.config = config diff --git a/app/initialize/state.js b/app/initialize/state.js index eb309525..82d2e730 100644 --- a/app/initialize/state.js +++ b/app/initialize/state.js @@ -63,6 +63,18 @@ const stepCache = observable.map({ func: () => api.connectWebsocketClient() }, + preventAccidentalClose: { + desc: 'Prevent accidental close', + func: () => { + window.onbeforeunload = function () { + if (config.preventAccidentalClose) { + return 'Do you really want to leave this site? Changes you made may not be saved.' + } + return void 0 + } + return true + } + } }) stepCache.insert = function (key, value, referKey, before = false) { From b59cc20c265a279d92781e181f90beda1204b945 Mon Sep 17 00:00:00 2001 From: hackape Date: Fri, 18 Aug 2017 17:47:43 +0800 Subject: [PATCH 2/6] add `@protectedObservable` decorator pattern --- app/commons/Tree/state.js | 11 +++--- app/utils/decorators/index.js | 3 +- app/utils/decorators/protectedObservable.js | 40 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 app/utils/decorators/protectedObservable.js diff --git a/app/commons/Tree/state.js b/app/commons/Tree/state.js index 0737f538..05223f57 100644 --- a/app/commons/Tree/state.js +++ b/app/commons/Tree/state.js @@ -1,5 +1,6 @@ import _ from 'lodash' import { observable, computed, action, autorun } from 'mobx' +import { protectedObservable } from 'utils/decorators' function TreeNodeScope () { const SHADOW_ROOT_NODE = 'SHADOW_ROOT_NODE' @@ -28,17 +29,13 @@ function TreeNodeScope () { state.entities.set(this.id, this) } - @observable _isDir = false - @observable _name = '' - @computed get name () { return this._name } - set name (v) { return this._name = v } - @computed get isDir () { return this._isDir } - set isDir (v) { return this._isDir = v } + @protectedObservable _name = '' + @protectedObservable _isDir = false + @protectedObservable _parentId = undefined @observable isFolded = true @observable isFocused = false @observable isHighlighted = false - @observable parentId = undefined @observable index = 0 @computed get isShadowRoot () { diff --git a/app/utils/decorators/index.js b/app/utils/decorators/index.js index d2aef227..d2984e5b 100644 --- a/app/utils/decorators/index.js +++ b/app/utils/decorators/index.js @@ -1,4 +1,5 @@ import mapEntityFactory from './mapEntity' import defaultProps from './defaultProps' +import protectedObservable from './protectedObservable' -export { mapEntityFactory, defaultProps } +export { mapEntityFactory, defaultProps, protectedObservable } diff --git a/app/utils/decorators/protectedObservable.js b/app/utils/decorators/protectedObservable.js new file mode 100644 index 00000000..56d7fbd7 --- /dev/null +++ b/app/utils/decorators/protectedObservable.js @@ -0,0 +1,40 @@ +import { observable, computed } from 'mobx' + +/* + * This decorator enforce a pattern that's widely used in this project, + * + * @protectedObservable _foo = 'bar' + * + * is a short hand for: + * + * @observable _foo = 'bar' + * @computed + * get foo () { return this._foo } + * set foo (value) { return this._foo = value } + * + * you can specify publicKey explicitly by calling: + * @protectedObservable('publicFoo') _foo = 'bar' + */ +function _protectedObservableDecorator (target, privateKey, descriptor, publicKey) { + if (!publicKey) publicKey = privateKey.replace(/^_/, '') + + const computedDescriptor = computed(target, publicKey, { + get () { return this[privateKey] }, + set (v) { return this[privateKey] = v }, + }) + + Object.defineProperty(target, publicKey, computedDescriptor) + + return observable(target, privateKey, descriptor) +} + +function protectedObservable (optionalPublicKey) { + if (typeof optionalPublicKey === 'string') { + return function protectedObservableDecorator (target, key, descriptor) { + return _protectedObservableDecorator(target, key, descriptor, optionalPublicKey) + } + } else { + return _protectedObservableDecorator.apply(null, arguments) + } +} +export default protectedObservable From bc0301ced3f1b28122124fdfd085d65eab11dfbd Mon Sep 17 00:00:00 2001 From: hackape Date: Fri, 18 Aug 2017 18:04:34 +0800 Subject: [PATCH 3/6] :bug: fix the bug in file tree state sync when file is deleted --- app/components/FileTree/actions.js | 6 +++--- app/components/FileTree/state.js | 31 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/app/components/FileTree/actions.js b/app/components/FileTree/actions.js index 9c824507..f5776c2c 100644 --- a/app/components/FileTree/actions.js +++ b/app/components/FileTree/actions.js @@ -67,9 +67,9 @@ export const toggleNodeFold = registerAction('filetree:toggle_node_fold', } ) -export const removeNode = registerAction('filetree:remove_node', - node => state.entities.delete(node.id) -) +export const removeNode = registerAction('filetree:remove_node', (node) => { + state.entities.delete(node.id || node.path) +}) export const openContextMenu = contextMenuStore.openContextMenuFactory(FileTreeContextMenuItems) export const closeContextMenu = contextMenuStore.closeContextMenu diff --git a/app/components/FileTree/state.js b/app/components/FileTree/state.js index 7329432c..738ae651 100644 --- a/app/components/FileTree/state.js +++ b/app/components/FileTree/state.js @@ -42,29 +42,29 @@ class FileTreeNode extends TreeNode { if (this.path === ROOT_PATH) this.isFolded = false } - @observable path = null - - // override default name / isDir behavior + /* override base class */ @computed get name () { - return this.file.name + return this.file ? this.file.name : '' } @computed get isDir () { - return this.file.isDir - } - - @computed get file () { - return FileState.entities.get(this.path) + return this.file ? this.file.isDir : false } - @computed get parent () { + @computed get parentId () { // prioritize corresponding file's tree node - if (this.file) { - const parentFile = this.file && this.file.parent - if (parentFile === null) return state.shadowRoot - return state.entities.get(parentFile.path) + if (this.file && this.file.parent) { + return this.file.parent.path } - return null + return this._parentId + } + /* end override */ + + /* extend base class */ + @observable path = null + + @computed get file () { + return FileState.entities.get(this.path) } @computed get children () { @@ -84,6 +84,7 @@ class FileTreeNode extends TreeNode { @computed get size () { if (this.file) return this.file.size } + /* end extend */ } export default state From 929553b3493393c18ad98b07b8c0dcdc9855cd4b Mon Sep 17 00:00:00 2001 From: hackape Date: Fri, 18 Aug 2017 19:07:27 +0800 Subject: [PATCH 4/6] :bug: handle file change ws notif correctly in place where file is being edited. --- app/commons/File/subscribeToFileChange.js | 25 ++++++++++--------- .../components/CodeEditor/BaseCodeEditor.jsx | 4 +++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/commons/File/subscribeToFileChange.js b/app/commons/File/subscribeToFileChange.js index 863f0274..8526d33e 100644 --- a/app/commons/File/subscribeToFileChange.js +++ b/app/commons/File/subscribeToFileChange.js @@ -43,6 +43,12 @@ function handleGitFiles (node) { return false } +// fixme: maybe we should make this a standard method of File model +function fileIsOpened (filePath) { + const openedFilePaths = mobxStore.EditorState.entities.values().map(editor => editor.filePath) + return openedFilePaths.includes(filePath) +} + export default function subscribeToFileChange () { autorun(() => { if (!config.fsSocketConnected) return @@ -51,27 +57,22 @@ export default function subscribeToFileChange () { client.subscribe(`/topic/ws/${config.spaceKey}/change`, (frame) => { const data = JSON.parse(frame.body) const node = data.fileInfo + switch (data.changeType) { case 'create': - if (handleGitFiles(node)) { - break - } - FileActions.loadNodeData([node]) - break case 'modify': if (handleGitFiles(node)) { break } - FileActions.loadNodeData([node]) - const tabsToUpdate = mobxStore.EditorTabState.tabs.values().filter(tab => tab.path === node.path) - if (tabsToUpdate.length) { + if (!node.isDir && fileIsOpened(node.path)) { api.readFile(node.path).then(({ content }) => { - TabActions.updateTabByPath({ - path: node.path, - content, - }) + node.content = content + FileActions.loadNodeData([node]) }) + } else { + FileActions.loadNodeData([node]) } + break case 'delete': FileActions.removeNode(node) diff --git a/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx b/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx index 1b9e2500..53c2824a 100644 --- a/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx +++ b/app/components/Editor/components/CodeEditor/BaseCodeEditor.jsx @@ -30,6 +30,10 @@ class BaseCodeEditor extends Component {
this.dom = r} style={{ width: '100%', height: '100%' }} /> ) } + + componentWillUnmount () { + this.editor.destroy() + } } BaseCodeEditor.propTypes = { From 3c0f4f0e3b26e823a3deace36a4340edadec23a9 Mon Sep 17 00:00:00 2001 From: hackape Date: Fri, 18 Aug 2017 19:23:58 +0800 Subject: [PATCH 5/6] :bug: hook up `config.fileExcludePatterns` to `settings.general.exclude_files` address Coding/WebIDE#179 --- app/settings.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/settings.js b/app/settings.js index 82a5ec2b..2852c4a7 100644 --- a/app/settings.js +++ b/app/settings.js @@ -1,7 +1,8 @@ import isObject from 'lodash/isObject' +import { observable, reaction, extendObservable, computed, action, autorunAsync } from 'mobx' +import config from 'config' import emitter, { THEME_CHANGED } from 'utils/emitter' import is from 'utils/is' -import { observable, reaction, extendObservable, computed, action, autorunAsync } from 'mobx' import dynamicStyle from 'utils/dynamicStyle' let EditorState @@ -159,7 +160,7 @@ const settings = observable({ extensions: new DomainSetting({}), general: new DomainSetting({ - _keys: ['language', 'hide_files'], + _keys: ['language', 'exclude_files'], requireConfirm: true, language: { name: 'settings.general.language', @@ -169,9 +170,12 @@ const settings = observable({ { name: 'settings.general.languageOption.chinese', value: 'Chinese' }, ] }, - hide_files: { + exclude_files: { name: 'settings.general.hideFiles', - value: '/.git,/.coding-ide' + value: config.fileExcludePatterns.join(','), + reaction (value) { + config.fileExcludePatterns = value.split(',') + } } }), From 214e49ae3e20198e7c4c3db274b833d276d7adb4 Mon Sep 17 00:00:00 2001 From: hackape Date: Fri, 18 Aug 2017 19:39:22 +0800 Subject: [PATCH 6/6] :bug: fix theme setting bug address Coding/WebIDE#179 --- app/settings.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/app/settings.js b/app/settings.js index 2852c4a7..1d1d2663 100644 --- a/app/settings.js +++ b/app/settings.js @@ -1,5 +1,5 @@ import isObject from 'lodash/isObject' -import { observable, reaction, extendObservable, computed, action, autorunAsync } from 'mobx' +import { observable, reaction, extendObservable, computed, action } from 'mobx' import config from 'config' import emitter, { THEME_CHANGED } from 'utils/emitter' import is from 'utils/is' @@ -15,7 +15,7 @@ export const UIThemeOptions = [ ] export const SyntaxThemeOptions = ['default', 'neo', 'eclipse', 'monokai', 'material'] -const changeTheme = (nextThemeId) => { +const changeUITheme = (nextThemeId) => { if (!window.themes) window.themes = {} if (UIThemeOptions.map(option => option.value).includes(nextThemeId)) { import(`!!style-loader/useable!css-loader!stylus-loader!./styles/${nextThemeId}/index.styl`) @@ -27,9 +27,10 @@ const changeTheme = (nextThemeId) => { }) } - if (nextThemeId === 'dark' && EditorState.options.theme === 'default') { + const editorTheme = EditorState.options.theme + if (nextThemeId === 'dark' && (editorTheme === 'default' || editorTheme === 'neo' || editorTheme === 'eclipse')) { settings.theme.syntax_theme.value = 'material' - } else if (nextThemeId === 'base-theme' && (EditorState.options.theme === 'monokai' || EditorState.options.theme === 'material')) { + } else if (nextThemeId === 'base-theme' && (editorTheme === 'monokai' || editorTheme === 'material')) { settings.theme.syntax_theme.value = 'default' } emitter.emit(THEME_CHANGED, nextThemeId) @@ -145,15 +146,14 @@ const settings = observable({ ui_theme: { name: 'settings.theme.uiTheme', value: 'base-theme', - options: UIThemeOptions + options: UIThemeOptions, + reaction: changeUITheme, }, syntax_theme: { name: 'settings.theme.syntaxTheme', value: 'default', options: SyntaxThemeOptions, - reaction (value) { - changeSyntaxTheme(value) - } + reaction: changeSyntaxTheme, } }), @@ -279,7 +279,3 @@ const settings = observable({ }) export default settings - -autorunAsync('changeTheme', () => { - changeTheme(settings.theme.ui_theme.value) -})