diff --git a/app/commands/commandBindings/file.js b/app/commands/commandBindings/file.js index 4c59a286..0c25de03 100644 --- a/app/commands/commandBindings/file.js +++ b/app/commands/commandBindings/file.js @@ -1,10 +1,11 @@ /* @flow weak */ import { bindActionCreators } from 'redux' import store, { getState, dispatch } from '../../store' +import mobxStore from '../../mobxStore' import { path as pathUtil } from '../../utils' import api from '../../backendAPI' import * as _Modal from '../../components/Modal/actions' -import * as Tab from '../../components/Tab' +import * as TabActions from 'commons/Tab/actions' import { notify } from '../../components/Notification/actions' const Modal = bindActionCreators(_Modal, dispatch) @@ -74,9 +75,9 @@ export default { }).then(createFolderAtPath) }, 'file:save': (c) => { - const { TabState } = getState() - const activeTab = Tab.selectors.getActiveTab(TabState) - const content = activeTab ? ide.editors[activeTab.id].getValue() : '' + const { EditorTabState } = mobxStore + const activeTab = EditorTabState.activeTab + const content = activeTab ? activeTab.editor.getValue() : '' if (!activeTab.path) { const createFile = createFileWithContent(content) @@ -86,17 +87,17 @@ export default { selectionRange: [1, '/untitled'.length] }) .then(createFile) - .then(path => dispatch(Tab.actions.updateTab({ + .then(path => dispatch(TabActions.updateTab({ id: activeTab.id, path, title: path.replace(/^.*\/([^\/]+$)/, '$1') }))) - .then(() => dispatch(Tab.actions.updateTabFlags(activeTab.id, 'modified', false))) + .then(() => dispatch(TabActions.updateTabFlags(activeTab.id, 'modified', false))) } else { api.writeFile(activeTab.path, content) .then(() => { - dispatch(Tab.actions.updateTabFlags(activeTab.id, 'modified', false)) - dispatch(Tab.actions.updateTab({ + dispatch(TabActions.updateTabFlags(activeTab.id, 'modified', false)) + dispatch(TabActions.updateTab({ id: activeTab.id, content: { body: content } })) }) diff --git a/app/commands/commandBindings/tab.js b/app/commands/commandBindings/tab.js index f6268a2d..5cf709db 100644 --- a/app/commands/commandBindings/tab.js +++ b/app/commands/commandBindings/tab.js @@ -1,24 +1,25 @@ /* @flow weak */ -import store, { dispatch as $d } from '../../store' -import * as Tab from '../../components/Tab/actions' -import * as PaneActions from '../../components/Pane/actions' +import { dispatch as $d } from '../../store' +import store from 'app/mobxStore' +import * as Tab from 'commons/Tab/actions' +import * as PaneActions from 'components/Pane/actions' export default { 'tab:close': c => { - $d(Tab.removeTab(c.context.id)) + Tab.removeTab(c.context.id) }, 'tab:close_other': c => { - $d(Tab.removeOtherTab(c.context.id)) + Tab.removeOtherTab(c.context.id) }, 'tab:close_all': c => { - $d(Tab.removeAllTab(c.context.id)) + Tab.removeAllTab(c.context.id) }, 'tab:split_v': c => { - const panes = store.getState().PaneState.panes - const pane = Object.values(panes).find((pane) => ( + const panes = store.PaneState.panes + const pane = panes.values().find(pane => ( pane.content && pane.content.type === 'tabGroup' && pane.content.id === c.context.tabGroupId )) $d(PaneActions.splitTo(pane.id, 'bottom')) diff --git a/app/commons/Tab/TabBar.jsx b/app/commons/Tab/TabBar.jsx new file mode 100644 index 00000000..03cf0947 --- /dev/null +++ b/app/commons/Tab/TabBar.jsx @@ -0,0 +1,118 @@ +import React, { Component, PropTypes } from 'react' +import cx from 'classnames' +import { observer } from 'mobx-react' +import { dnd } from 'utils' +import { defaultProps } from 'utils/decorators' +import TabLabel from './TabLabel' +import Menu from 'components/Menu' +import ContextMenu from 'components/ContextMenu' + +@defaultProps(props => ({ + addTab: () => props.tabGroup.addTab(), +})) +@observer +class TabBar extends Component { + static propTypes = { + tabGroup: PropTypes.object.isRequired, + contextMenuItems: PropTypes.array.isRequired, + addTab: PropTypes.func, + closePane: PropTypes.func, + }; + + constructor (props) { + super(props) + this.state = { + showDropdownMenu: false, + showContextMenu: false, + contextMenuPos: {}, + contextMenuContext: null, + } + } + + render () { + const { + tabGroup, + addTab, + contextMenuItems, + } = this.props + + const tabBarId = `tab_bar_${tabGroup.id}` + return ( +
+ + {dnd.target.id === tabBarId ?
: null} +
+ +
+
{ e.stopPropagation(); this.setState({ showDropdownMenu: true }) }} + > + + {this.renderDropdownMenu()} +
+ this.setState({ showContextMenu: false })} + /> +
+ ) + } + + openContextMenu = (e, context) => { + e.stopPropagation() + e.preventDefault() + + this.setState({ + showContextMenu: true, + contextMenuPos: { x: e.clientX, y: e.clientY }, + contextMenuContext: context, + }) + } + + renderDropdownMenu () { + if (!this.state.showDropdownMenu) return null + const dropdownMenuItems = this.makeDropdownMenuItems() + if (!dropdownMenuItems.length) return null + return this.setState({ showDropdownMenu: false })} + /> + + } + + makeDropdownMenuItems = () => { + let baseItems = this.props.tabGroup.siblings.length === 0 ? [] + : [{ + name: 'Close Pane', + command: this.props.closePane, + }] + const tabs = this.props.tabGroup.tabs + const tabLabelsItem = tabs && tabs.map(tab => ({ + name: tab.title || 'untitled', + command: e => tab.activate(), + })) + + if (tabLabelsItem.length) { + if (!baseItems.length) return tabLabelsItem + return baseItems.concat({ name: '-' }, tabLabelsItem) + } else { + return baseItems + } + } +} + +export default TabBar diff --git a/app/commons/Tab/TabContent.jsx b/app/commons/Tab/TabContent.jsx new file mode 100644 index 00000000..f576158c --- /dev/null +++ b/app/commons/Tab/TabContent.jsx @@ -0,0 +1,20 @@ +import React, { PropTypes } from 'react' +import cx from 'classnames' +import { observer } from 'mobx-react' + +export const TabContent = ({ children }) => ( +
+
{children}
+
+) + +export const TabContentItem = observer(({ tab, children }) => ( +
+ {children} +
+)) + +TabContentItem.propTypes = { + tab: PropTypes.object.isRequired, +} + diff --git a/app/commons/Tab/TabLabel.jsx b/app/commons/Tab/TabLabel.jsx new file mode 100644 index 00000000..1ee61645 --- /dev/null +++ b/app/commons/Tab/TabLabel.jsx @@ -0,0 +1,49 @@ +import React, { Component, PropTypes } from 'react' +import cx from 'classnames' +import { observer } from 'mobx-react' +import { dnd } from 'utils' +import { defaultProps } from 'utils/decorators' +import { dispatch } from '../../store' + +let TabLabel = observer(({tab, removeTab, activateTab, openContextMenu}) => { + const tabLabelId = `tab_label_${tab.id}` + return ( +
  • activateTab(tab.id)} + onDragStart={e => dnd.dragStart({ type: 'TAB', id: tab.id }) } + onContextMenu={e => openContextMenu(e, tab)} + > + {dnd.target.id === tabLabelId ?
    : null} + {tab.icon ?
    : null} +
    {tab.title}
    +
    + { e.stopPropagation(); removeTab(tab.id) }}>× + +
    +
  • + ) +}) + +TabLabel.propTypes = { + tab: PropTypes.object.isRequired, + removeTab: PropTypes.func.isRequired, + activateTab: PropTypes.func.isRequired, + openContextMenu: PropTypes.func.isRequired, +} + +TabLabel = defaultProps(props => ({ + activateTab: function () { + props.tab.activate() + }, + removeTab: function () { + props.tab.destroy() + }, +}))(TabLabel) + +export default TabLabel diff --git a/app/components/Tab/actions.js b/app/commons/Tab/actions.js similarity index 98% rename from app/components/Tab/actions.js rename to app/commons/Tab/actions.js index 19fcbcd6..858779d3 100644 --- a/app/components/Tab/actions.js +++ b/app/commons/Tab/actions.js @@ -1,5 +1,5 @@ /* @flow weak */ -import { createAction } from 'redux-actions' +import { createAction } from 'utils/actions' export const TAB_DISSOLVE_GROUP = 'TAB_DISSOLVE_GROUP' diff --git a/app/commons/Tab/index.js b/app/commons/Tab/index.js new file mode 100644 index 00000000..9c7218b5 --- /dev/null +++ b/app/commons/Tab/index.js @@ -0,0 +1,5 @@ +import TabStateScope from './state' +import TabBar from './TabBar' +import { TabContent, TabContentItem } from './TabContent' + +export { TabBar, TabContent, TabContentItem, TabStateScope } diff --git a/app/commons/Tab/state.js b/app/commons/Tab/state.js new file mode 100644 index 00000000..488491b9 --- /dev/null +++ b/app/commons/Tab/state.js @@ -0,0 +1,149 @@ +import _ from 'lodash' +import { observable, computed, action, autorun } from 'mobx' +import { mapEntityFactory } from 'utils/decorators' + +function TabScope () { + +const entities = observable({ + tabs: observable.map({}), + tabGroups: observable.map({}), + activeTabGroupId: null, + get activeTabGroup () { + let activeTabGroup = this.tabGroups.get(this.activeTabGroupId) + if (!activeTabGroup) return this.tabGroups.values()[0] + return activeTabGroup + }, + get activeTab () { + const activeTabGroup = this.activeTabGroup + if (!activeTabGroup) return this.tabs.values()[0] + return activeTabGroup.activeTab + } +}) + +const mapEntity = mapEntityFactory(entities) + +class Tab { + constructor (config={}) { + this.id = config.id || _.uniqueId('tab_') + entities.tabs.set(this.id, this) + } + + @observable title = 'untitled' + @observable index = 0 + @observable tabGroupId = '' + @observable flags = {} + + @computed get tabGroup () { + return entities.tabGroups.get(this.tabGroupId) + } + + @computed get isActive () { + return this.tabGroup.activeTab === this + } + + @computed get siblings () { + return this.tabGroup.tabs + } + + @computed get next () { + return this.siblings[this.index + 1] + } + + @computed get prev () { + return this.siblings[this.index - 1] + } + + getAdjacent (checkNextFirst) { + let adjacent = checkNextFirst ? + (this.next || this.prev) : (this.prev || this.next) + return adjacent + } + + @action activate () { + this.tabGroup.activeTabId = this.id + this.tabGroup.activate() + } + + @action destroy () { + this.tabGroup.removeTab(this) + entities.tabs.delete(this.id) + } +} + +autorun(() => { + entities.tabGroups.forEach(tabGroup => { + // correct tab index + tabGroup.tabs.forEach((tab, tabIndex) => { + if (tab.index !== tabIndex) tab.index = tabIndex + }) + }) +}) + + +class TabGroup { + constructor (config={}) { + this.id = config.id || _.uniqueId('tab_group_') + entities.tabGroups.set(this.id, this) + } + + @observable activeTabId = null + + @computed get tabs () { + return entities.tabs.values() + .filter(tab => tab.tabGroupId === this.id) + .sort((a, b) => a.index - b.index) + } + + @computed get activeTab () { + let activeTab = entities.tabs.get(this.activeTabId) + if (activeTab && activeTab.tabGroupId === this.id) { + return activeTab + } + return null + } + + @computed get siblings () { + return entities.tabGroups.values() + } + + @computed get isActive () { + return entities.activeTabGroup === this + } + + @mapEntity('tabs') + @action addTab (tab, insertIndex = this.tabs.length) { + if (!tab) tab = new Tab() + tab.index = insertIndex + tab.tabGroupId = this.id + tab.activate() + return tab + } + + @action activate () { + entities.activeTabGroupId = this.id + } + + @mapEntity('tabs') + @action activateTab (tab) { + tab.activate() + } + + @mapEntity('tabs') + @action removeTab (tab) { + if (tab.isActive) { + let adjacentTab = tab.getAdjacent() + if (adjacentTab) adjacentTab.activate() + } + tab.tabGroupId = null + } + + @action destroy () { + entities.tabGroups.delete(this.id) + } +} + +return { Tab, TabGroup, entities } + +} + +export default TabScope diff --git a/app/components/Breadcrumbs/index.jsx b/app/components/Breadcrumbs/index.jsx index ebeaea81..9155ac29 100644 --- a/app/components/Breadcrumbs/index.jsx +++ b/app/components/Breadcrumbs/index.jsx @@ -2,7 +2,6 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import config from '../../config' -import * as Tab from '../Tab' let Breadcrumbs = ({ fileNode }) => { const pathComps = fileNode.path.split('/') @@ -24,7 +23,7 @@ let Breadcrumbs = ({ fileNode }) => { ) } Breadcrumbs = connect(state => { - const activeTab = Tab.selectors.getActiveTab(state.TabState) + const activeTab = state.EditorTabState.activeTab const currentPath = activeTab ? activeTab.path : '' let fileNode = state.FileTreeState.nodes[currentPath] if (!fileNode) fileNode = state.FileTreeState.nodes[''] // fallback to rootNode diff --git a/app/components/CodeMirrorEditor/index.jsx b/app/components/CodeMirrorEditor/index.jsx index 5adc8070..1d850ff9 100644 --- a/app/components/CodeMirrorEditor/index.jsx +++ b/app/components/CodeMirrorEditor/index.jsx @@ -3,11 +3,46 @@ import React, {Component} from 'react'; import CodeMirror from 'codemirror'; import {connect} from 'react-redux'; import './addons'; -import { dispatchCommand } from '../../commands' +import dispatchCommand from '../../commands/dispatchCommand' import _ from 'lodash' -import * as TabActions from '../Tab/actions'; +import * as TabActions from 'commons/Tab/actions'; + +function initializeEditor (editorContainer, theme) { + // @todo: add other setting item from config + const editorDOM = document.createElement('div') + Object.assign(editorDOM.style, { width: '100%', height: '100%' }) + editorContainer.appendChild(editorDOM) + const editor = CodeMirror(editorDOM, { + theme, + autofocus: true, + lineNumbers: true, + matchBrackets: true, + autoCloseBrackets: true, + }) + + // 1. resize + editor.setSize('100%', '100%') + + // 2. prevent default codemirror dragover handler, so the drag-to-split feature can work + // but the default handler that open a file on drop is actually pretty neat, + // should make our drag feature compatible with it later + editor.on('dragover', (a, e) => e.preventDefault()) + + editor.isFocused = editor.hasFocus // little hack to make codemirror work with legacy interface + return editor +} + +// Ref: codemirror/mode/meta.js +function getMode (tab) { + return CodeMirror.findModeByMIME(tab.contentType) || CodeMirror.findModeByFileName(tab.path.split('/').pop()) +} const debounced = _.debounce(func => func(), 1000) + +@connect(state => ({ + setting: state.SettingState.views.tabs.EDITOR, + themeSetting: state.SettingState.views.tabs.THEME, +})) class CodeMirrorEditor extends Component { static defaultProps = { theme: 'default', @@ -15,58 +50,52 @@ class CodeMirrorEditor extends Component { width: '100%', }; - constructor(props) { - super(props); - this.state = {}; + constructor (props) { + super(props) + this.state = {} } - componentDidMount() { - const {themeSetting, tab, width, height} = this.props; - const themeConfig = themeSetting.items[1].value.split('/').pop(); + componentDidMount () { + const { themeSetting, tab } = this.props; + const themeConfig = themeSetting.items[1].value + let editorInitialized = false // todo add other setting item from config - const editor = this.editor = CodeMirror(this.editorDOM, { - theme: themeConfig, - autofocus: true, - lineNumbers: true, - matchBrackets: true, - autoCloseBrackets: true, - }); + if (tab.editor) { + this.editor = tab.editor + this.editorContainer.appendChild(this.editor.getWrapperElement()) + } else { + this.editor = tab.editor = initializeEditor(this.editorContainer, themeConfig) + editorInitialized = true + } + const editor = this.editor // @fixme: // related counterparts: // 1. IdeEnvironment.js // 2. commandBindings/file.js window.ide.editors[tab.id] = editor - // 1. resize - editor.setSize(width, height); - - // 2. prevent default codemirror dragover handler, so the drag-to-split feature can work - // but the default handler that open a file on drop is actually pretty neat, - // should make our drag feature compatible with it later - editor.on('dragover', e => e.preventDefault()) - - if (tab.content) { - const body = tab.content.body; - const modeInfo = this.getMode(tab); - if (body) editor.setValue(body); + if (editorInitialized && tab.path && tab.content) { + const body = tab.content.body + const modeInfo = getMode(tab) + if (body) editor.setValue(body) if (modeInfo) { - let mode = modeInfo.mode; + let mode = modeInfo.mode if (mode === 'null') { editor.setOption('mode', mode) } else { - require([`codemirror/mode/${mode}/${mode}.js`], () => editor.setOption('mode', mode)); + require([`codemirror/mode/${mode}/${mode}.js`], () => editor.setOption('mode', mode)) } } } - editor.focus(); - editor.isFocused = editor.hasFocus; // little hack to make codemirror work with legacy interface - editor.on('change', this.onChange); - editor.on('focus', () => this.props.dispatch(TabActions.activateTab(tab.id))) + + editor.focus() + editor.on('change', this.onChange) + editor.on('focus', this.onFocus) } - // Ref: codemirror/mode/meta.js - getMode (tab) { - return CodeMirror.findModeByMIME(tab.contentType) || CodeMirror.findModeByFileName(tab.path.split('/').pop()) + componentWillUnmount () { + this.editor.off('change', this.onChange) + this.editor.off('focus', this.onFocus) } onChange = (e) => { @@ -81,33 +110,76 @@ class CodeMirrorEditor extends Component { dispatchCommand('file:save') this.isChanging = false }) - }; + } + + onFocus = () => { + this.props.dispatch(TabActions.activateTab(this.props.tab.id)) + } - componentWillReceiveProps ({ tab }) { + componentWillReceiveProps ({ tab, themeSetting }) { if (tab.flags.modified || !this.editor || !tab.content) return if (tab.content.body !== this.editor.getValue()) { this.editor.setValue(tab.content.body) } + + const nextTheme = themeSetting.items[1].value + const theme = this.props.themeSetting.items[1].value + if (theme !== nextTheme) this.editor.setOption('theme', nextTheme) } - render() { - const {width, height} = this.props; - const name = this.state.name; - const divStyle = {width, height}; + render () { + const {width, height} = this.props + const name = this.state.name + const divStyle = { width, height } return ( -
    this.editorDOM = c } - id={name} - style={divStyle} - >
    - ); +
    this.editorContainer = c} + id={name} + style={divStyle} + /> + ) } } -CodeMirrorEditor = connect(state => ({ + +@connect(state => ({ setting: state.SettingState.views.tabs.EDITOR, themeSetting: state.SettingState.views.tabs.THEME, -}) -)(CodeMirrorEditor); +})) +class TablessCodeMirrorEditor extends Component { + constructor (props) { + super(props) + this.state = {} + } + + componentDidMount() { + const { themeSetting, width, height } = this.props + const theme = themeSetting.items[1].value + + this.editor = initializeEditor(this.editorContainer, theme) + this.editor.focus() + this.editor.on('change', this.onChange) + } + + onChange = (e) => { + this.props.dispatch(TabActions.createTabInGroup(this.props.tabGroupId, { + flags: { modified: true }, + content: this.editor.getValue() + })) + } + + componentWillReceiveProps ({ themeSetting }) { + const nextTheme = themeSetting.items[1].value + const theme = this.props.themeSetting.items[1].value + if (theme !== nextTheme) this.editor.setOption('theme', nextTheme) + } + + render () { + return ( +
    this.editorContainer = c } style={{ height: '100%', width: '100%' }} /> + ) + } +} -export default CodeMirrorEditor; +export default CodeMirrorEditor +export { TablessCodeMirrorEditor } diff --git a/app/components/DragAndDrop/index.jsx b/app/components/DragAndDrop.jsx similarity index 78% rename from app/components/DragAndDrop/index.jsx rename to app/components/DragAndDrop.jsx index 2642d351..661fef31 100644 --- a/app/components/DragAndDrop/index.jsx +++ b/app/components/DragAndDrop.jsx @@ -1,13 +1,18 @@ -/* @flow weak */ import React, { Component } from 'react' -import { connect } from 'react-redux' -import _ from 'lodash' -import { updateDragOverTarget, updateDragOverMeta, dragEnd, dragStart } from './actions' -import * as PaneActions from '../Pane/actions' -import * as TabActions from '../Tab/actions' -import * as FileTreeActions from '../FileTree/actions' - -@connect(state => state.DragAndDrop) +import { dispatch } from '../store' +import { observer } from 'mobx-react' +import { dnd } from 'utils' +import * as PaneActions from './Pane/actions' +import * as TabActions from 'commons/Tab/actions' +import * as FileTreeActions from './FileTree/actions' + +// Corner case: file dragging doesn't trigger 'dragend' natively +// so need to patch for this behavior +function isFileDragEnd (e) { + return (e.screenX === 0 && e.screenY === 0 && e.dataTransfer.files.length) +} + +@observer class DragAndDrop extends Component { constructor (props) { @@ -15,7 +20,7 @@ class DragAndDrop extends Component { } render () { - const {isDragging, meta, target} = this.props + const { isDragging, meta, target } = dnd if (!isDragging) return null if (meta && meta.paneLayoutOverlay) { @@ -41,22 +46,20 @@ class DragAndDrop extends Component { window.ondragend = this.onDragEnd window.ondragleave = this.onDragLeave } + onDragLeave = (e) => { e.preventDefault() - const {source = {}, dispatch} = this.props - if (source.type && source.type === 'EXTERNAL_FILE') { - setTimeout(() => dispatch(dragEnd()), 1000) - } + if (isFileDragEnd(e)) dnd.dragEnd() } + onDragOver = (e) => { e.preventDefault() - const {source, droppables = [], dispatch, meta} = this.props - const prevTarget = this.props.target + const { source, droppables = [], meta } = dnd if (!source) { - dispatch(dragStart({ - sourceType: 'EXTERNAL_FILE', - sourceId: Date.now(), - })) + dnd.dragStart({ + type: 'EXTERNAL_FILE', + id: Date.now(), + }) } const [oX, oY] = [e.pageX, e.pageY] const target = droppables.reduce((result, droppable) => { @@ -69,9 +72,9 @@ class DragAndDrop extends Component { } }, null) if (!target) return - // if (!prevTarget || target.id !== prevTarget.id) { - dispatch(updateDragOverTarget({id: target.id, type: target.type })) - // } + + dnd.dragOver({ id: target.id, type: target.type }) + switch (`${source.type}_on_${target.type}`) { case 'TAB_on_PANE': return this.dragTabOverPane(e, target) @@ -87,7 +90,7 @@ class DragAndDrop extends Component { onDrop = (e) => { e.preventDefault() - const {source, target, meta, dispatch} = this.props + const { source, target, meta } = dnd if (!source || !target) return switch (`${source.type}_on_${target.type}`) { case 'TAB_on_PANE': @@ -110,22 +113,20 @@ class DragAndDrop extends Component { default: } - dispatch(dragEnd()) + dnd.dragEnd() } onDragEnd = (e) => { e.preventDefault() - const {dispatch} = this.props - dispatch(dragEnd()) + dnd.dragEnd() } dragTabOverTabBar (e, target) { - const {dispatch} = this.props if (target.type !== 'TABLABEL' && target.type !== 'TABBAR') return if (target.type === 'TABLABEL') { - dispatch(updateDragOverMeta({tabLabelTargetId: target.id})) + dnd.updateDragOverMeta({tabLabelTargetId: target.id}) } else { - dispatch(updateDragOverMeta({tabBarTargetId: target.id})) + dnd.updateDragOverMeta({tabBarTargetId: target.id}) } } @@ -138,7 +139,7 @@ class DragAndDrop extends Component { } dragTabOverPane (e, target) { if (target.type !== 'PANE') return - const {meta, dispatch} = this.props + const { meta } = dnd const [oX, oY] = [e.pageX, e.pageY] const {top, left, right, bottom, height, width} = target.rect @@ -207,10 +208,10 @@ class DragAndDrop extends Component { } } - dispatch(updateDragOverMeta({ + dnd.updateDragOverMeta({ paneSplitDirection: overlayPos, paneLayoutOverlay: overlay - })) + }) } } diff --git a/app/components/DragAndDrop/actions.js b/app/components/DragAndDrop/actions.js deleted file mode 100644 index 94f92116..00000000 --- a/app/components/DragAndDrop/actions.js +++ /dev/null @@ -1,16 +0,0 @@ -/* @flow weak */ -import { createAction } from 'redux-actions' - -export const DND_DRAG_START = 'DND_DRAG_START' -export const dragStart = createAction(DND_DRAG_START, - ({sourceType, sourceId}) => ({sourceType, sourceId}) -) - -export const DND_DRAG_OVER = 'DND_DRAG_OVER' -export const updateDragOverTarget = createAction(DND_DRAG_OVER, target => target) - -export const DND_UPDATE_DRAG_OVER_META = 'DND_UPDATE_DRAG_OVER_META' -export const updateDragOverMeta = createAction(DND_UPDATE_DRAG_OVER_META, meta => meta) - -export const DND_DRAG_END = 'DND_DRAG_END' -export const dragEnd = createAction(DND_DRAG_END) diff --git a/app/components/DragAndDrop/reducer.js b/app/components/DragAndDrop/reducer.js deleted file mode 100644 index 364a0142..00000000 --- a/app/components/DragAndDrop/reducer.js +++ /dev/null @@ -1,69 +0,0 @@ -/* @flow weak */ -import _ from 'lodash' -import { handleActions } from 'redux-actions' -import { - DND_DRAG_START, - DND_DRAG_OVER, - DND_UPDATE_DRAG_OVER_META, - DND_DRAG_END -} from './actions' - -function getDroppables () { - var droppables = _.map(document.querySelectorAll('[data-droppable]'), (DOMNode) => { - return { - id: DOMNode.id, - DOMNode: DOMNode, - type: DOMNode.getAttribute('data-droppable'), - rect: DOMNode.getBoundingClientRect() - } - }) - return droppables -} - -export default handleActions({ - [DND_DRAG_START]: (state, action) => { - const {sourceType, sourceId} = action.payload - return { - isDragging: true, - source: { - type: sourceType, - id: sourceId - }, - droppables: getDroppables() - } - }, - - [DND_DRAG_OVER]: (state, action) => { - return { - ...state, - target: action.payload - } - }, - - [DND_UPDATE_DRAG_OVER_META]: (state, action) => { - return { - ...state, - meta: action.payload - } - }, - - [DND_DRAG_END]: (state, action) => { - return {isDragging: false} - } -}, {isDragging: false}) - - -/* -@StateShape: -{ - isDragging: - source: { - id: - type: - } - target: { - id: - type: - } -} -*/ diff --git a/app/components/Editor/TabContainer.jsx b/app/components/Editor/TabContainer.jsx new file mode 100644 index 00000000..e842f38b --- /dev/null +++ b/app/components/Editor/TabContainer.jsx @@ -0,0 +1,66 @@ +import _ from 'lodash'; +import React, { Component, PropTypes } from 'react' +import cx from 'classnames' +import { observer, inject } from 'mobx-react' +import { TabBar, TabContent, TabContentItem } from 'commons/Tab' +import * as TabActions from 'commons/Tab/actions'; +import EditorWrapper from '../EditorWrapper' +import { TablessCodeMirrorEditor } from '../CodeMirrorEditor' + +const contextMenuItems = [ + { + name: 'Close', + icon: '', + command: 'tab:close' + }, { + name: 'Close Others', + icon: '', + command: 'tab:close_other' + }, { + name: 'Close All', + icon: '', + command: 'tab:close_all' + }, + { name: '-' }, + { + name: 'Vertical Split', + icon: '', + command: 'tab:split_v' + }, { + name: 'Horizontal Split', + icon: '', + command: 'tab:split_h' + } +] + +@observer +class TabContainer extends Component { + static propTypes = { + containingPaneId: PropTypes.string, + tabGroup: PropTypes.object, + createGroup: PropTypes.func, + }; + + render () { + const tabGroup = this.props.tabGroup + if (!tabGroup) return null + return ( +
    + + + {tabGroup.tabs.length ? tabGroup.tabs.map(tab => + + + + ) + : + + + } + +
    + ) + } +} + +export default TabContainer diff --git a/app/components/Editor/index.js b/app/components/Editor/index.js new file mode 100644 index 00000000..0073d5a7 --- /dev/null +++ b/app/components/Editor/index.js @@ -0,0 +1,3 @@ +import TabContainer from './TabContainer' + +export default TabContainer diff --git a/app/components/Editor/reducer.js b/app/components/Editor/reducer.js new file mode 100644 index 00000000..8c6a0a90 --- /dev/null +++ b/app/components/Editor/reducer.js @@ -0,0 +1,124 @@ +import { extendObservable, createTransformer, action } from 'mobx' +import { handleActions } from 'utils/actions' +import EditorTabState, { Tab, TabGroup } from './state' +import store from 'app/mobxStore' +import { + TAB_CREATE, + TAB_CREATE_IN_GROUP, + TAB_REMOVE, + TAB_ACTIVATE, + TAB_CREATE_GROUP, + TAB_REMOVE_GROUP, + TAB_UPDATE, + TAB_UPDATE_FLAGS, + TAB_UPDATE_BY_PATH, + TAB_MOVE_TO_GROUP, + TAB_MOVE_TO_PANE, + TAB_INSERT_AT, + TAB_REMOVE_OTHER, + TAB_REMOVE_ALL, +} from 'commons/Tab/actions' + +const renew = createTransformer(state => { + return { + ...state, + tabGroups: state.tabGroups.toJS(), + tabs: state.tabs.toJS(), + activeTab: state.activeTab, + activeTabGroup: state.activeTabGroup, + } +}) + +const TabActionHandler = handleActions({ + [TAB_CREATE]: (state, payload) => { + const tabConfig = payload + const tab = new Tab(tabConfig) + const activeTabGroup = state.activeTabGroup + activeTabGroup.addTab(tab) + }, + + [TAB_CREATE_IN_GROUP]: (state, payload) => { + const { groupId, tab: tabConfig } = payload + const tab = new Tab(tabConfig) + state.tabGroups.get(groupId).addTab(tab) + }, + + [TAB_REMOVE]: (state, payload) => { + const tab = state.tabs.get(payload) + tab.destroy() + }, + + [TAB_ACTIVATE]: (state, payload) => { + const tab = state.tabs.get(payload) + tab.activate() + }, + + [TAB_REMOVE_OTHER]: (state, payload) => { + const tab = state.tabs.get(payload) + tab.activate() + tab.tabGroup.tabs.forEach(eachTab => { + if (eachTab !== tab) eachTab.destroy() + }) + }, + + [TAB_REMOVE_ALL]: (state, payload) => { + const tab = state.tabs.get(payload) + tab.tabGroup.tabs.forEach(tab => tab.destroy()) + }, + + [TAB_CREATE_GROUP]: (state, payload) => { + const { groupId } = payload + new TabGroup({ id: groupId }) + }, + + [TAB_REMOVE_GROUP]: (state, payload) => { + const tab = state.tabs.get(payload) + }, + + [TAB_UPDATE]: (state, payload) => { + const tabId = payload.id + const tab = state.tabs.get(tabId) + if (tab) extendObservable(tab, payload) + }, + + [TAB_UPDATE_FLAGS]: (state, { tabId, flags }) => { + const tab = state.tabs.get(tabId) + tab.flags = flags + }, + + [TAB_MOVE_TO_GROUP]: (state, { tabId, groupId }) => { + const tab = state.tabs.get(tabId) + const tabGroup = state.tabGroups.get(groupId) + if (!tab || !tabGroup) return + tabGroup.addTab(tab) + }, + + [TAB_INSERT_AT]: (state, { tabId, beforeTabId }) => { + const tab = state.tabs.get(tabId) + const anchorTab = state.tabs.get(beforeTabId) + const prev = anchorTab.prev + const insertIndex = (prev) ? (anchorTab.index + prev.index) / 2 : -1 + tab.tabGroup.addTab(tab, insertIndex) + } +}, EditorTabState) + +const TabReducer = action((s, action) => { + return renew(EditorTabState) +}) + +export default TabReducer + +const crossActionHandlers = handleActions({ + [TAB_MOVE_TO_PANE]: (allState, { tabId, paneId }) => { + const pane = allState.PaneState.panes.get(paneId) + const tab = allState.EditorTabState.tabs.get(tabId) + tab.tabGroup.removeTab(tab) + pane.tabGroup.addTab(tab) + return allState + } +}, store) + + +export const TabCrossReducer = (state, action) => { + return state +} diff --git a/app/components/Editor/state.js b/app/components/Editor/state.js new file mode 100644 index 00000000..0ec52e68 --- /dev/null +++ b/app/components/Editor/state.js @@ -0,0 +1,28 @@ +import { extendObservable, observable, computed } from 'mobx' +import { TabStateScope } from 'commons/Tab' +const { Tab: BaseTab, TabGroup: BaseTabGroup, entities: state } = TabStateScope() +import PaneState from 'components/Pane/state' + +class Tab extends BaseTab { + constructor (config={}) { + super(config) + extendObservable(this, config) + } + + @observable path = '' + @observable content = {} +} + +class TabGroup extends BaseTabGroup { + constructor (config={}) { + super(config) + extendObservable(this, config) + } + + @computed get pane () { + return PaneState.panes.values().find(pane => pane.contentId === this.id) + } +} + +export default state +export { Tab, TabGroup } diff --git a/app/components/ImageEditor/index.jsx b/app/components/EditorWrapper/Editors/ImageEditor.jsx similarity index 97% rename from app/components/ImageEditor/index.jsx rename to app/components/EditorWrapper/Editors/ImageEditor.jsx index 249558b4..d4a3f984 100644 --- a/app/components/ImageEditor/index.jsx +++ b/app/components/EditorWrapper/Editors/ImageEditor.jsx @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react' -import config from '../../config' +import config from 'config' import { request } from 'utils' const previewPic = 'https://dn-coding-net-production-static.qbox.me/static/5d487aa5c207cf1ca5a36524acb953f1.gif' diff --git a/app/components/UnknownEditor/index.jsx b/app/components/EditorWrapper/Editors/UnknownEditor.jsx similarity index 97% rename from app/components/UnknownEditor/index.jsx rename to app/components/EditorWrapper/Editors/UnknownEditor.jsx index 6895dee0..7597194d 100644 --- a/app/components/UnknownEditor/index.jsx +++ b/app/components/EditorWrapper/Editors/UnknownEditor.jsx @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from 'react' import filesize from 'filesize' -import config from '../../config' +import config from 'config' class UnknownEditor extends Component { constructor (props) { diff --git a/app/components/EditorWrapper/Editors/WelcomeEditor.js b/app/components/EditorWrapper/Editors/WelcomeEditor.js new file mode 100644 index 00000000..043621d5 --- /dev/null +++ b/app/components/EditorWrapper/Editors/WelcomeEditor.js @@ -0,0 +1,5 @@ +import React, { Component, PropTypes } from 'react' + +export default function () { + return
    Welcome
    +} diff --git a/app/components/EditorWrapper/index.jsx b/app/components/EditorWrapper/index.jsx index f8eced84..a118a3c9 100644 --- a/app/components/EditorWrapper/index.jsx +++ b/app/components/EditorWrapper/index.jsx @@ -1,9 +1,10 @@ import React, { PropTypes } from 'react' -import MarkdownEditor from '../MarkdownEditor' -import ImageEditor from '../ImageEditor' import CodeMirrorEditor from '../CodeMirrorEditor' -import UnknownEditor from '../UnknownEditor' -import * as Tab from '../Tab' +import MarkdownEditor from '../MarkdownEditor' +import ImageEditor from './Editors/ImageEditor' +import UnknownEditor from './Editors/UnknownEditor' +import WelcomeEditor from './Editors/WelcomeEditor' +import { getTabType } from 'utils' const editors = { CodeMirrorEditor, @@ -42,9 +43,9 @@ const EditorWrapper = ({ tab }, { i18n }) => { const { path = '' } = tab let type = 'default' if (tab.contentType) { - if (Tab.types.getTabType(tab) === 'IMAGE') { + if (getTabType(tab) === 'IMAGE') { type = 'imageEditor' - } else if (Tab.types.getTabType(tab) === 'UNKNOWN') { + } else if (getTabType(tab) === 'UNKNOWN') { type = 'unknownEditor' } } diff --git a/app/components/FileTree/actions.js b/app/components/FileTree/actions.js index 4cab0508..e2263b25 100644 --- a/app/components/FileTree/actions.js +++ b/app/components/FileTree/actions.js @@ -1,7 +1,8 @@ import _ from 'lodash' import { createAction } from 'redux-actions' import api from '../../backendAPI' -import * as Tab from '../Tab' +import * as TabActions from 'commons/Tab/actions' +import { getTabType } from 'utils' import { updateUploadProgress } from '../StatusBar/actions' export const FILETREE_SELECT_NODE = 'FILETREE_SELECT_NODE' @@ -23,13 +24,13 @@ export function openNode (node, shouldBeFolded = null, deep = false) { dispatch(toggleNodeFold(node, shouldBeFolded, deep)) } } else { - const tabType = Tab.types.getTabType(node) + const tabType = getTabType(node) if ( - Tab.types.getTabType(node) === 'TEXT' + getTabType(node) === 'TEXT' ) { api.readFile(node.path) .then(data => { - dispatch(Tab.actions.createTab({ + dispatch(TabActions.createTab({ id: _.uniqueId('tab_'), type: 'editor', title: node.name, @@ -44,7 +45,7 @@ export function openNode (node, shouldBeFolded = null, deep = false) { })) }) } else { - dispatch(Tab.actions.createTab({ + dispatch(TabActions.createTab({ id: _.uniqueId('tab_'), type: 'editor', title: node.name, diff --git a/app/components/FileTree/subscribeToFileChange.js b/app/components/FileTree/subscribeToFileChange.js index 5ebddb86..cdb734b1 100644 --- a/app/components/FileTree/subscribeToFileChange.js +++ b/app/components/FileTree/subscribeToFileChange.js @@ -2,8 +2,9 @@ import config from '../../config' import api from '../../backendAPI' import store, { dispatch } from '../../store' +import mobxStore from '../../mobxStore' import * as FileTreeActions from './actions' -import { actions as TabActions, selectors as TabSelectors } from '../Tab' +import * as TabActions from 'commons/Tab/actions' export default function subscribeToFileChange () { return api.websocketConnectedPromise.then(client => @@ -16,7 +17,7 @@ export default function subscribeToFileChange () { break case 'modify': dispatch(FileTreeActions.loadNodeData([node])) - var tabsToUpdate = TabSelectors.getTabsByPath(store.getState().TabState, node.path) + var tabsToUpdate = mobxStore.EditorTabState.tabs.values().filter(tab => tab.path === node.path) if (tabsToUpdate.length) { api.readFile(node.path).then(({ content }) => { dispatch(TabActions.updateTabByPath({ diff --git a/app/components/Modal/FilePalette/component.jsx b/app/components/Modal/FilePalette/component.jsx index 708d8cce..a8a856d4 100644 --- a/app/components/Modal/FilePalette/component.jsx +++ b/app/components/Modal/FilePalette/component.jsx @@ -3,7 +3,7 @@ import _ from 'lodash' import React, { Component } from 'react' import api from '../../../backendAPI' import store, { dispatch as $d } from '../../../store' -import * as Tab from '../../Tab' +import * as TabActions from 'commons/Tab/actions' import cx from 'classnames' import { dispatchCommand } from '../../../commands/lib/keymapper' @@ -70,7 +70,7 @@ class FilePalette extends Component { api.readFile(node.path) .then(data => { - $d(Tab.actions.createTab({ + $d(TabActions.createTab({ id: _.uniqueId('tab_'), type: 'editor', title: node.name || filename, diff --git a/app/components/Pane/Pane.jsx b/app/components/Pane/Pane.jsx index bb44cd0d..aebc8ac7 100644 --- a/app/components/Pane/Pane.jsx +++ b/app/components/Pane/Pane.jsx @@ -1,18 +1,18 @@ /* @flow weak */ import React, { PropTypes } from 'react' -import { bindActionCreators } from 'redux' -import { connect } from 'react-redux' +import { observer } from 'mobx-react' import cx from 'classnames' import { confirmResize } from './actions' -import TabContainer from '../Tab' +import TabContainer from '../Editor' import EditorWrapper from '../EditorWrapper' import PaneAxis from './PaneAxis' import ResizeBar from '../ResizeBar' -const _Pane = (props) => { - const { pane, parentFlexDirection, confirmResize } = props +const Pane = observer(props => { + const { pane, parentFlexDirection } = props const style = { flexGrow: pane.size, display: pane.display } + return (
    { > {pane.views.length // priortize `pane.views` over `pane.content` ? :
    - +
    } { parentFlexDirection={parentFlexDirection} />
    ) -} +}) -_Pane.propTypes = { +Pane.propTypes = { pane: PropTypes.object, parentFlexDirection: PropTypes.string, } -const Pane = connect( - (state, { paneId }) => ({ pane: state.PaneState.panes[paneId] }), - dispatch => bindActionCreators({ confirmResize }, dispatch) -)(_Pane) - export default Pane diff --git a/app/components/Pane/PaneAxis.jsx b/app/components/Pane/PaneAxis.jsx index 57da8f83..f8a135c8 100644 --- a/app/components/Pane/PaneAxis.jsx +++ b/app/components/Pane/PaneAxis.jsx @@ -1,8 +1,10 @@ /* @flow weak */ import React, { Component, PropTypes } from 'react' +import { observer } from 'mobx-react' import cx from 'classnames' import Pane from './Pane' +@observer class PaneAxis extends Component { static propTypes = { id: PropTypes.string, @@ -20,20 +22,20 @@ class PaneAxis extends Component { onResizing (listener) { if (typeof listener === 'function') { this.resizingListeners.push(listener) } } render () { - const { pane } = this.props + const selfPane = this.props.pane let Subviews - if (pane.views.length) { - Subviews = pane.views.map(paneId => - + if (selfPane.views.length) { + Subviews = selfPane.views.map(pane => + ) } else { - Subviews = + Subviews = } return (
    {Subviews}
    ) diff --git a/app/components/Pane/PanesContainer.jsx b/app/components/Pane/PanesContainer.jsx index e3829edf..73b4b504 100644 --- a/app/components/Pane/PanesContainer.jsx +++ b/app/components/Pane/PanesContainer.jsx @@ -1,16 +1,15 @@ /* @flow weak */ import React, { Component } from 'react' -import { connect } from 'react-redux' +import { inject } from 'mobx-react' import PaneAxis from './PaneAxis' -import store from '../../store.js' -var PrimaryPaneAxis = connect(state => { - let rootPane = state.PaneState.panes[state.PaneState.rootPaneId] +var PrimaryPaneAxis = inject(state => { + let rootPane = state.PaneState.rootPane return { pane: rootPane } })(PaneAxis) -var PanesContainer = (props) => { - return +var PanesContainer = () => { + return } export default PanesContainer diff --git a/app/components/Pane/actions.js b/app/components/Pane/actions.js index 73665a01..d74a5103 100644 --- a/app/components/Pane/actions.js +++ b/app/components/Pane/actions.js @@ -4,8 +4,8 @@ import { getPrevSibling, } from './selectors' -import { createAction } from 'redux-actions' -import { promiseActionMixin } from '../../utils' +import { createAction } from 'utils/actions' +import { promiseActionMixin } from 'utils' export const PANE_INITIALIZE = 'PANE_INITIALIZE' export const PANE_UNSET_COVER = 'PANE_UNSET_COVER' @@ -27,9 +27,7 @@ export const split = createAction(PANE_SPLIT_WITH_KEY, ) export const PANE_SPLIT = 'PANE_SPLIT' -export const splitTo = promiseActionMixin( - createAction(PANE_SPLIT, (paneId, splitDirection) => ({paneId, splitDirection})) -) +export const splitTo = createAction.promise(PANE_SPLIT, (paneId, splitDirection) => ({paneId, splitDirection})) export const PANE_UPDATE = 'PANE_UPDATE' export const updatePane = createAction(PANE_UPDATE) diff --git a/app/components/Pane/reducer.js b/app/components/Pane/reducer.js index c5dc612b..5653abd7 100644 --- a/app/components/Pane/reducer.js +++ b/app/components/Pane/reducer.js @@ -1,7 +1,7 @@ -/* @flow weak */ import _ from 'lodash' -import { update } from '../../utils' -import { handleActions } from 'redux-actions' +import { createTransformer } from 'mobx' +import { handleActions } from 'utils/actions' +import entities, { Pane } from './state' import { PANE_UPDATE, PANE_SPLIT, @@ -9,113 +9,16 @@ import { PANE_CLOSE, PANE_CONFIRM_RESIZE, } from './actions' -import { - getPaneById, - getParent, - getNextSibling, - getPrevSibling, - getPanesWithPosMap, -} from './selectors' -const debounced = _.debounce(func => func(), 50) - -/** - * The state shape: - * - * PaneState = { - rootPaneId: PropTypes.string, - panes: { - [pane_id]: { - id: PropTypes.string, - flexDirection: PropTypes.string, - size: PropTypes.number, - parentId: PropTypes.string, - views: PropTypes.arrayOf(PropTypes.string), - content: PropTypes.shape({ - type: PropTypes.string, - id: PropTypes.string, - }) - } - } - } -*/ - -const Pane = (paneConfig) => { - const defaults = { - id: _.uniqueId('pane_view_'), - flexDirection: 'row', - size: 100, - views: [], - parentId: '', - content: undefined, - } - - return { ...defaults, ...paneConfig } -} - -const rootPane = Pane({ - id: 'pane_view_1', - flexDirection: 'row', - size: 100, - views: [], - content: { - type: 'tabGroup', - id: '' - } -}) -const defaultState = { - panes: { - [rootPane.id]: rootPane - }, - rootPaneId: rootPane.id -} - -const _addSiblingAfterPane = (state, pane, siblingPane) => _addSibling(state, pane, siblingPane, 1) -const _addSiblingBeforePane = (state, pane, siblingPane) => _addSibling(state, pane, siblingPane, -1) -const _addSibling = (state, pane, siblingPane, indexOffset) => { - let parent = getParent(state, pane) - let atIndex = parent.views.indexOf(pane.id) + indexOffset - parent.views.splice(atIndex, 0, siblingPane.id) - - return update(state, { - panes: { - [parent.id]: {$set: {...parent}}, - [siblingPane.id]: {$set: siblingPane} - } - }) -} - -function _hoistSingleChild (state, parent) { - if (parent.views.length != 1) return state - const singleChild = state.panes[parent.views[0]] - if (singleChild.views.length > 0) { - parent = update(parent, { - views: {$set: singleChild.views}, - flexDirection: {$set: singleChild.flexDirection} - }) - } else { - parent = update(parent, { - views: {$set: []}, - content: {$set: singleChild.content} - }) - } - let nextState = update(state, {panes: {[parent.id]: {$set: parent}}}) - nextState = update(nextState, {panes: {$delete: singleChild.id}}) - return _hoistSingleChild(nextState, parent) -} -export default handleActions({ - [PANE_UPDATE]: (state, action) => { - const { id: paneId, tabGroupId } = action.payload - let pane = getPaneById(state, paneId) - - return update(state, { - panes: {[pane.id]: {content: {id: {$set: tabGroupId}}}} - }) +const actionHandlers = handleActions({ + [PANE_UPDATE]: (state, { id: paneId, tabGroupId }) => { + const pane = state.panes.get(paneId) + pane.contentId = tabGroupId }, - [PANE_SPLIT]: (state, action) => { - const { paneId, splitDirection } = action.payload - let pane = getPaneById(state, paneId) + [PANE_SPLIT]: (state, { paneId, splitDirection }, action) => { + console.log(PANE_SPLIT + ' start'); + const pane = state.panes.get(paneId) /* ----- */ let flexDirection, newPane if (splitDirection === 'center') { @@ -133,105 +36,69 @@ export default handleActions({ flexDirection = 'column' break default: - throw 'Pane.splitToDirection method requires param "splitDirection"' + throw Error('Pane.splitToDirection method requires param "splitDirection"') } - let parent = getParent(state, pane) + const parent = pane.parent // If flexDirection is same as parent's, // then we can simply push the newly splitted view into parent's "views" array + // debugger if (parent && parent.flexDirection === flexDirection) { - newPane = Pane({ parentId: parent.id, content: {type: 'tabGroup'} }) - action.meta.resolve(newPane.id) + newPane = new Pane({ parentId: parent.id }) if (splitDirection === 'right' || splitDirection === 'bottom') { - return _addSiblingAfterPane(state, pane, newPane) + // return _addSiblingAfterPane(state, pane, newPane) + newPane.index = pane.index + 0.5 } else { - return _addSiblingBeforePane(state, pane, newPane) + newPane.index = pane.index - 0.5 } - + action.meta.resolve(newPane.id) // If flexDirection is NOT the same as parent's, // that means we should spawn a child pane } else { - pane = {...pane, flexDirection} - let spawnedChild = Pane({parentId: pane.id, content: {...pane.content}}) - delete pane.content - newPane = Pane({ parentId: pane.id, content: {type: 'tabGroup'} }) + pane.flexDirection = flexDirection + const spawnedChild = new Pane({ parentId: pane.id, contentId: pane.contentId }) + pane.contentId = null + newPane = new Pane({ parentId: pane.id }) if (splitDirection === 'right' || splitDirection === 'bottom') { - pane.views = [spawnedChild.id, newPane.id] + spawnedChild.index = 0 + newPane.index = 1 } else { - pane.views = [newPane.id, spawnedChild.id] + spawnedChild.index = 1 + newPane.index = 0 + console.log(newPane.index, spawnedChild.index); } action.meta.resolve(newPane.id) - - return update(state, { - panes: { - [newPane.id]: {$set: newPane}, - [pane.id]: {$set: pane}, - [spawnedChild.id]: {$set: spawnedChild}, - } - }) } }, - [PANE_CLOSE]: (state, action) => { - const { paneId, targetTabGroupId, sourceTabGroupId } = action.payload - let parent = getParent(state, paneId) - let nextState = state + [PANE_CLOSE]: (state, { paneId }) => { + let pane = state.panes.get(paneId) + let parent = pane.parent // the `mergeTabGroups` part of the action is handled inside `Tab/reducer.js` - parent = update(parent, {views: {$without: paneId}}) - nextState = update(nextState, {panes: {[parent.id]: {$set: parent}}}) - nextState = _hoistSingleChild(nextState, parent) - parent = nextState.panes[parent.id] - nextState = update(nextState, {panes: {[parent.id]: {$set: parent}}}) - nextState = update(nextState, {panes: {$delete: paneId}}) - return nextState + // if parent is about to have only one child left + // we short-circut parent.content to the-pane-to-delete.content + if (parent.views.length === 2) parent.contentId = pane.contentId + entities.panes.delete(pane.id) }, - [PANE_CONFIRM_RESIZE]: (state, { payload: { leftView, rightView } }) => { - return update(state, { - panes: { - [leftView.id]: { size: { $set: leftView.size } }, - [rightView.id]: { size: { $set: rightView.size } }, - } - }) - } -}, defaultState) - - -export const PaneCrossReducer = handleActions({ - [PANE_SPLIT_WITH_KEY]: (allStates, action) => { - return allStates - // const { PaneState, TabState } = allStates - // const { splitCount, flexDirection } = action.payload - // let rootPane = PaneState.panes[PaneState.rootPaneId] - - // if ( - // (splitCount === rootPane.views.length && flexDirection === rootPane.flexDirection) || - // (splitCount === 1 && rootPane.views.length === 0 && rootPane.content) - // ) { - // return allStates - // } - - // // if rootPane has children - // if (rootPane.views.length) { - - // if (splitCount > rootPane.views.length) { - // // this is the easier case where we simply increase panes - // rootPane.views + [PANE_CONFIRM_RESIZE]: (state, { leftView, rightView }) => { + state.panes[leftView.id].size = leftView.size + state.panes[rightView.id].size = rightView.size + }, +}, entities) - // } else { - // // this is the harder case where we need to merge tabGroups - // } - // } +const transform = createTransformer(entities => { + return { + panes: entities.panes.toJS(), + rootPaneId: entities.rootPaneId, } }) +export default function (state, action) { + return transform(entities) +} - - - - - - +export const PaneCrossReducer = f => f diff --git a/app/components/Pane/state.js b/app/components/Pane/state.js new file mode 100644 index 00000000..2e2e56ab --- /dev/null +++ b/app/components/Pane/state.js @@ -0,0 +1,81 @@ +import uniqueId from 'lodash/uniqueId' +import { extendObservable, observable, computed, autorun } from 'mobx' +import EditorTabState, { TabGroup } from 'components/Editor/state' + +const state = observable({ + panes: observable.map({}), + activePaneId: null, + rootPaneId: null, + get rootPane () { + const rootPane = this.panes.get(this.rootPaneId) + return rootPane || this.panes.values()[0] + }, + get activePane () { + const activePane = this.panes.get(this.activePaneId) + return activePane || this.rootPane + }, +}) + +class BasePane { + constructor (paneConfig) { + const defaults = { + id: uniqueId('pane_view_'), + flexDirection: 'row', + size: 100, + parentId: '', + index: 0, + } + + paneConfig = Object.assign({}, defaults, paneConfig) + extendObservable(this, paneConfig) + state.panes.set(this.id, this) + } + + @computed + get parent () { + return state.panes.get(this.parentId) + } + + @computed + get views () { + return state.panes.values() + .filter(pane => pane.parentId === this.id) + .sort((a, b) => a.index - b.index) + } +} + +class Pane extends BasePane { + constructor (paneConfig) { + super(paneConfig) + this.contentType = 'tabGroup' + const tabGroup = this.tabGroup || new TabGroup() + this.contentId = tabGroup.id + } + + @observable contentId = '' + + @computed + get tabGroup () { + return EditorTabState.tabGroups.get(this.contentId) + } +} + +const rootPane = new Pane({ + id: 'pane_view_1', + flexDirection: 'row', + size: 100, +}) + +state.panes.set(rootPane.id, rootPane) +state.rootPaneId = rootPane.id + +autorun(() => { + state.panes.forEach(parentPane => + parentPane.views.forEach((pane, index) => { + if (pane.index !== index) pane.index = index + }) + ) +}) + +export default state +export { Pane } diff --git a/app/components/Panel/PanelContent.jsx b/app/components/Panel/PanelContent.jsx index 4ff201fd..f7f0baa8 100644 --- a/app/components/Panel/PanelContent.jsx +++ b/app/components/Panel/PanelContent.jsx @@ -6,8 +6,7 @@ import StatusBar from '../StatusBar' import PanesContainer from '../Pane' import FileTree from '../FileTree' import ExtensionPanelContent from './ExtensionPanelContent' -import Terminal from '../Terminal' -import TabContainer from '../Tab' +import TerminalContainer from '../Terminal' import SideBar, { SideBar2 } from './SideBar' import { SidePanelContainer, SidePanelView } from './SidePanel' import GitHistoryView from '../Git/GitHistoryView' @@ -47,11 +46,10 @@ const PanelContent = ({ panel }) => { ) case 'PANEL_BOTTOM': - // return return ( - + diff --git a/app/components/Panel/PanelsContainer.js b/app/components/Panel/PanelsContainer.js index 2a2e92ca..56f64651 100644 --- a/app/components/Panel/PanelsContainer.js +++ b/app/components/Panel/PanelsContainer.js @@ -2,13 +2,7 @@ import React, { Component } from 'react' import { connect } from 'react-redux' -import store from '../../store.js' import PanelAxis from './PanelAxis' -import * as PanelActions from './actions' -import PanesContainer from '../Pane' -import TabContainer from '../Tab' -import Terminal from '../Terminal' -import FileTree from '../FileTree' const PrimaryPanelAxis = connect(state => ({panel: state.PanelState.panels[state.PanelState.rootPanelId]}) diff --git a/app/components/Setting/reducer.js b/app/components/Setting/reducer.js index 3490c731..08bdc412 100644 --- a/app/components/Setting/reducer.js +++ b/app/components/Setting/reducer.js @@ -1,7 +1,7 @@ /* @flow weak */ import { handleActions } from 'redux-actions' import { OrderedMap } from 'immutable' -import { changeTheme, changeCodeTheme } from '../../utils/themeManager' +import { changeTheme } from '../../utils/themeManager' import { SETTING_ACTIVATE_TAB, SETTING_UPDATE_FIELD, @@ -123,7 +123,6 @@ export default handleActions({ [SETTING_UPDATE_FIELD]: (state, action) => { const { domain, fieldName, value } = action.payload if (fieldName === 'UI Theme') { changeTheme(value); } - if (fieldName === 'Syntax Theme') { changeCodeTheme(value); } return { ...state, views: { ...state.views, diff --git a/app/components/Tab/TabBar.jsx b/app/components/Tab/TabBar.jsx deleted file mode 100644 index 86f7a9e4..00000000 --- a/app/components/Tab/TabBar.jsx +++ /dev/null @@ -1,203 +0,0 @@ -import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import cx from 'classnames'; -import { dragStart } from '../DragAndDrop/actions'; -import Menu from '../Menu' -import * as TabActions from './actions'; -import * as PaneActions from '../Pane/actions'; -import ContextMenu from '../ContextMenu' - -const dividItem = { name: '-' } -const items = [ - { - name: 'Close', - icon: '', - command: 'tab:close' - }, { - name: 'Close Others', - icon: '', - command: 'tab:close_other' - }, { - name: 'Close All', - icon: '', - command: 'tab:close_all' - } -] -const itemsSplit = [ - dividItem, - { - name: 'Vertical Split', - icon: '', - command: 'tab:split_v' - }, { - name: 'Horizontal Split', - icon: '', - command: 'tab:split_h' - } -] - -class _TabBar extends Component { - constructor (props) { - super(props) - this.state = { - showDropdownMenu: false - } - } - - static propTypes = { - tabGroupId: PropTypes.string, - tabIds: PropTypes.array, - isDraggedOver: PropTypes.bool, - addTab: PropTypes.func, - closePane: PropTypes.func, - isRootPane: PropTypes.bool - } - - makeDropdownMenuItems = () => { - let baseItems = this.props.isRootPane ? [] - : [{ - name: 'Close Pane', - command: this.props.closePane, - }] - const tabs = this.props.tabs - const tabLabelsItem = tabs && tabs.map(tab => ({ - name: tab.title || 'untitled', - command: e => this.props.activateTab(tab.id) - })) - - if (tabLabelsItem.length) { - return baseItems.concat({name: '-'}, tabLabelsItem) - } else { - return baseItems - } - } - - renderDropdownMenu () { - const dropdownMenuItems = this.makeDropdownMenuItems() - if (this.state.showDropdownMenu && dropdownMenuItems.length) { - return this.setState({showDropdownMenu: false})} - /> - } else { - return null - } - } - - render () { - const { tabIds, tabGroupId, isRootPane, addTab, closePane, isDraggedOver, contextMenu, closeContextMenu, defaultContentType } = this.props - let menuItems - if (defaultContentType === 'terminal') { - menuItems = items - } else { - menuItems = items.concat(itemsSplit) - } - return ( -
    -
      - { tabIds && tabIds.map(tabId => - - ) } -
    - {isDraggedOver ?
    : null} -
    - -
    -
    {e.stopPropagation();this.setState({showDropdownMenu: true})}} - > - - {this.renderDropdownMenu()} -
    - -
    - ) - } -} - -const TabBar = connect((state, { tabIds, tabGroupId, containingPaneId }) => ({ - tabs: tabIds.map(tabId => state.TabState.tabs[tabId]), - isDraggedOver: state.DragAndDrop.meta - ? state.DragAndDrop.meta.tabBarTargetId === `tab_bar_${tabGroupId}` - : false, - isRootPane: state.PaneState.rootPaneId === containingPaneId, - contextMenu: state.TabState.contextMenu -}), (dispatch, { tabGroupId, containingPaneId }) => ({ - activateTab: (tabId) => dispatch(TabActions.activateTab(tabId)), - addTab: () => dispatch(TabActions.createTabInGroup(tabGroupId)), - closePane: () => dispatch(PaneActions.closePane(containingPaneId)), - closeContextMenu: () => dispatch(TabActions.closeContextMenu()) -}) -)(_TabBar) - - -const _TabLabel = ({tab, tabGroupId, isActive, isDraggedOver, removeTab, activateTab, dragStart, openContextMenu}) => { - const possibleStatus = { - 'modified': '*', - 'warning': '!', - 'offline': '*', - 'sync': '[-]', - 'loading': - } - - return ( -
  • activateTab(tab.id)} - draggable='true' - onDragStart={e => dragStart({sourceType: 'TAB', sourceId: tab.id})} - onContextMenu={e => openContextMenu(e, tab, tabGroupId)} - > - {isDraggedOver ?
    : null} - {tab.icon ?
    : null} -
    {tab.title}
    -
    - { e.stopPropagation(); removeTab(tab.id) }}>× - -
    -
  • - ) -} - -_TabLabel.propTypes = { - tab: PropTypes.object, - tabGroupId: PropTypes.string, - isDraggedOver: PropTypes.bool, - removeTab: PropTypes.func, - activateTab: PropTypes.func, - dragStart: PropTypes.func, - openContextMenu: PropTypes.func, - closeContextMenu: PropTypes.func -} - -const TabLabel = connect((state, { tabId }) => { - const tab = state.TabState.tabs[tabId] - const isActive = state.TabState.tabGroups[tab.tabGroupId].activeTabId === tabId - const isDraggedOver = state.DragAndDrop.meta - ? state.DragAndDrop.meta.tabLabelTargetId === `tab_label_${tabId}` - : false - return { isDraggedOver, tab, isActive } -}, dispatch => ({ - removeTab: (tabId) => dispatch(TabActions.removeTab(tabId)), - activateTab: (tabId) => dispatch(TabActions.activateTab(tabId)), - dragStart: (dragEventObj) => dispatch(dragStart(dragEventObj)), - openContextMenu: (e, node, tabGroupId) => dispatch(TabActions.openContextMenu(e, node, tabGroupId)), -}) -)(_TabLabel) - - -export default TabBar diff --git a/app/components/Tab/TabContainer.jsx b/app/components/Tab/TabContainer.jsx deleted file mode 100644 index d0059c04..00000000 --- a/app/components/Tab/TabContainer.jsx +++ /dev/null @@ -1,56 +0,0 @@ -/* @flow weak */ -import _ from 'lodash'; -import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import cx from 'classnames'; -import * as TabActions from './actions'; -import * as PaneActions from '../Pane/actions'; -import { dragStart } from '../DragAndDrop/actions'; -import TabBar from './TabBar' -import TabContent from './TabContent' - -class TabContainer extends Component { - constructor (props) { - super(props) - this.state = { - tabGroupId: props.tabGroupId || _.uniqueId('tab_group_'), - type: props.defaultContentType - } - } - - componentWillMount () { - const { tabGroups, createGroup, updatePane, defaultContentType, containingPaneId } = this.props - const { tabGroupId } = this.state - const tabGroup = tabGroups[tabGroupId] - if (!tabGroup) createGroup(tabGroupId, defaultContentType) - if (containingPaneId) updatePane({ id: containingPaneId, tabGroupId }) - } - - componentDidMount () { - if (this.props.defaultContentType == 'terminal') setTimeout(() => this.props.addTab(this.state.tabGroupId), 0) - } - - render () { - const tabGroup = this.props.tabGroups[this.state.tabGroupId] - if (!tabGroup) return null - return ( -
    - - -
    - ) - } -} - -export default connect((state, { tabGroupId }) => ({ - tabGroups: state.TabState.tabGroups -}), dispatch => ({ - createGroup: (tabGroupId, defaultContentType) => - dispatch(TabActions.createGroup(tabGroupId, defaultContentType)), - addTab: (tabGroupId) => dispatch(TabActions.createTabInGroup(tabGroupId)), - updatePane: (updatePatch) => dispatch(PaneActions.updatePane(updatePatch)) -}) -)(TabContainer) diff --git a/app/components/Tab/TabContent.jsx b/app/components/Tab/TabContent.jsx deleted file mode 100644 index 38b8aac7..00000000 --- a/app/components/Tab/TabContent.jsx +++ /dev/null @@ -1,47 +0,0 @@ -/* @flow weak */ -import React, { Component, PropTypes } from 'react'; -import { connect } from 'react-redux'; -import cx from 'classnames'; -import { dragStart } from '../DragAndDrop/actions'; -import * as TabActions from './actions'; - -const _TabContent = ({tabIds, defaultContentClass}) => { - let tabContentItems = tabIds.map(tabId => - - ) - return ( -
    -
      - {tabContentItems.length - ? tabContentItems - :
      - } -
    -
    - ) -} - -_TabContent.propTypes = { - tabs: PropTypes.array, -} - -const TabContent = connect((state, { tabIds }) => ({ - tabs: tabIds.map(tabId => state.TabState.tabs[tabId]), -}) -)(_TabContent) - - -const _TabContentItem = ({ tab, isActive, defaultContentClass }) => { - return
    - {React.createElement(defaultContentClass, { tab })} -
    -} - -const TabContentItem = connect((state, { tabId }) => { - const tab = state.TabState.tabs[tabId] - const isActive = state.TabState.tabGroups[tab.tabGroupId].activeTabId === tabId - return { tab, isActive } -} -)(_TabContentItem) - -export default TabContent diff --git a/app/components/Tab/index.js b/app/components/Tab/index.js deleted file mode 100644 index 661dbf0f..00000000 --- a/app/components/Tab/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export default from './TabContainer' -export * as actions from './actions' -export * as selectors from './selectors' -export * as types from './types' diff --git a/app/components/Tab/reducer.js b/app/components/Tab/reducer.js deleted file mode 100644 index ef1bf442..00000000 --- a/app/components/Tab/reducer.js +++ /dev/null @@ -1,345 +0,0 @@ -/* @flow weak */ -import _ from 'lodash' -import { update, Model } from '../../utils' -import { handleActions } from 'redux-actions' -import { - TAB_CREATE, - TAB_CREATE_IN_GROUP, - TAB_REMOVE, - TAB_ACTIVATE, - TAB_CREATE_GROUP, - TAB_REMOVE_GROUP, - TAB_UPDATE, - TAB_UPDATE_FLAGS, - TAB_UPDATE_BY_PATH, - TAB_MOVE_TO_GROUP, - TAB_MOVE_TO_PANE, - TAB_INSERT_AT, - TAB_CONTEXT_MENU_OPEN, - TAB_CONTEXT_MENU_CLOSE, - TAB_REMOVE_OTHER, - TAB_REMOVE_ALL -} from './actions' - -import { - getTabGroupOfTab, - getNextSiblingOfTab, - getActiveTabGroup, - getActiveTabOfTabGroup, - getActiveTab, - isActive, - getTabsByPath, -} from './selectors' - -/** - * The state shape: - * - * TabState: { - activeTabGroupId: PropTypes.string, - tabGroups: { - [tab_group_id]: { - id: PropTypes.string, - type: PropTypes.string, - tabIds: PropTypes.arrayOf(PropTypes.string), - activeTabId: PropTypes.string - } - }, - tabs: { - [tab_id]: { - id: PropTypes.string, - flags: PropTypes.object, - icon: PropTypes.string, - title: PropTypes.string, - path: PropTypes.string, - content: PropTypes.object, - tabGroupId: PropTypes.string - } - } - } -*/ - -const defaultState = { - tabGroups: {}, - tabs: {}, - activeTabGroupId: '', - contextMenu: { - isActive: false, - pos: { x: 0, y: 0 }, - contextNode: null, - } -} - -const Tab = Model({ - id: '', - type: 'editor', - flags: {}, - icon: 'fa fa-folder-', - title: 'untitled', - path: '', - content: null, - tabGroupId: '' -}) - -const TabGroup = Model({ - id: '', - tabIds: [], - type: 'editor', // default content type - activeTabId: '' -}) - -const _activateTabGroup = (state, tabGroup) => { - if (isActive(state, tabGroup)) return state - return update(state, { - activeTabGroupId: {$set: tabGroup.id} - }) -} - -const _activateTab = (state, tab) => { - if (!tab) return state - const tabGroup = getTabGroupOfTab(state, tab) - if (isActive(state, tab) && isActive(state, tabGroup)) return state - - let nextState = update(state, { - tabGroups: {[tabGroup.id]: {activeTabId: {$set: tab.id}}} - }) - return _activateTabGroup(nextState, tabGroup) -} - -const _removeTab = (state, tab) => { - let nextState = state - if (isActive(state, tab)) { /* activateNextTab */ - let nextTab = getNextSiblingOfTab(state, tab) - if (nextTab) nextState = _activateTab(state, nextTab) - } - - return update(nextState, { - tabGroups: { - [tab.tabGroupId]: { - tabIds: {$removeValue: tab.id} - } - }, - tabs: {$delete: tab.id} - }) -} - -const _removeOtherTab = (state, tab) => { - let nextState = state - nextState = _activateTab(state, tab) - const tabGroup = state.tabGroups[tab.tabGroupId] - const tabIdsToBeDeleted = _.without(tabGroup.tabIds, tab.id) - const nextTabs = _.omit(state.tabs, tabIdsToBeDeleted) - return update(nextState, { - tabGroups: { - [tab.tabGroupId]: { - tabIds: { $set: [tab.id] } - } - }, - tabs: { $set: nextTabs } - }) -} - -const _removeAllTab = (state, tab) => { - const tabGroup = state.tabGroups[tab.tabGroupId] - const tabIdsToBeDeleted = tabGroup.tabIds - const nextTabs = _.omit(state.tabs, tabIdsToBeDeleted) - return update(state, { - tabGroups: { - [tab.tabGroupId]: { - tabIds: { $set: [] } - } - }, - tabs: { $set: nextTabs } - }) -} - -const _moveTabToGroup = (state, tab, tabGroup, insertIndex = tabGroup.tabIds.length) => { - // 1. remove it from original group - let nextState = _removeTab(state, tab) - if (tabGroup.id === tab.tabGroupId) tabGroup = nextState.tabGroups[tabGroup.id] - // 2. add it to new group - tab = update(tab, {tabGroupId: {$set: tabGroup.id}}) - let tabIds = tabGroup.tabIds.slice() - tabIds.splice(insertIndex, 0 , tab.id) - - nextState = update(nextState, { - tabGroups: {[tabGroup.id]: {tabIds: {$set: tabIds}}}, - tabs: {[tab.id]: {$set: tab}} - }) - - return _activateTab(nextState, tab) -} - -const _createTabInGroup = (state, tabConfig, tabGroup) => { - tabGroup = state.tabGroups[tabGroup.id] - const newTab = Tab({ - id: _.uniqueId('tab_'), - tabGroupId: tabGroup.id, - ...tabConfig - }) - let nextState = update(state, { - tabs: {[newTab.id]: {$set: newTab}}, - tabGroups: {[tabGroup.id]: {tabIds: {$push: [newTab.id]}}} - }) - return _activateTab(nextState, newTab) -} - -const _mergeTabGroups = (state, targetTabGroupId, sourceTabGroupIds) => { - if (!_.isArray(sourceTabGroupIds)) sourceTabGroupIds = [sourceTabGroupIds] - let targetTabGroup = state.tabGroups[targetTabGroupId] - let sourceTabGroups = sourceTabGroupIds.map(id => state.tabGroups[id]) - let tabIdsToBeMerged = sourceTabGroups.reduce((tabIds, tabGroup) => { - return tabIds.concat(tabGroup.tabIds) - }, []) - let targetTabIds = targetTabGroup.tabIds.concat(tabIdsToBeMerged) - let mergedTabs = tabIdsToBeMerged.reduce((tabs, tabId) => { - tabs[tabId] = {...state.tabs[tabId], tabGroupId: targetTabGroup.id} - return tabs - }, {}) - let nextState = update(state, { - tabGroups: { - [targetTabGroup.id]: {tabIds: {$set: targetTabIds}} - }, - tabs: {$merge: mergedTabs}, - activeTabGroupId: {$set: targetTabGroup.id} - }) - return update(nextState, {tabGroups: {$delete: sourceTabGroupIds}}) -} - -const TabReducer = handleActions({ - - [TAB_CREATE]: (state, action) => { - const tabConfig = action.payload - // here we try our best to put the tab into the right group - // first we try the current active group, check if it's of same type - // if not, we try to find the group of same type as tab, - // if can find none, well, we fallback to the current active group we found - let tabGroup = getActiveTabGroup(state) - if (tabGroup.type !== tabConfig.type) { - let _tabGroup = _(state.tabGroups).find(g => g.type === tabConfig.type) - if (_tabGroup) tabGroup = _tabGroup - } - return _createTabInGroup(state, tabConfig, tabGroup) - }, - - [TAB_CREATE_IN_GROUP]: (state, action) => { - const { groupId, tab: tabConfig } = action.payload - let tabGroup = state.tabGroups[groupId] - return _createTabInGroup(state, tabConfig, tabGroup) - }, - - [TAB_REMOVE]: (state, action) => { - const tab = state.tabs[action.payload] - return _removeTab(state, tab) - }, - - [TAB_REMOVE_OTHER]: (state, action) => { - const tab = state.tabs[action.payload] - return _removeOtherTab(state, tab) - }, - - [TAB_REMOVE_ALL]: (state, action) => { - const tab = state.tabs[action.payload] - return _removeAllTab(state, tab) - }, - - [TAB_ACTIVATE]: (state, action) => { - const tab = state.tabs[action.payload] - let nextState = _activateTab(state, tab) - return nextState - }, - - [TAB_CREATE_GROUP]: (state, action) => { - const {groupId, defaultContentType} = action.payload - const newTabGroup = TabGroup({ id: groupId, type: defaultContentType }) - let nextState = update(state,{ - tabGroups: {[newTabGroup.id]: {$set: newTabGroup}} - }) - nextState = _activateTabGroup(nextState, newTabGroup) - return nextState - }, - - [TAB_REMOVE_GROUP]: (state, action) => { - // 还有 group active 的问题 - return update(state, { - tabGroups: {$delete: action.payload} - }) - }, - - [TAB_UPDATE]: (state, action) => { - const tabConfig = action.payload - if (!state.tabs[tabConfig.id]) return state - return update(state, { - tabs: {[tabConfig.id]: {$merge: tabConfig || {} }} - }) - }, - - [TAB_UPDATE_FLAGS]: (state, action) => { - const {tabId, flags} = action.payload - return update(state, { - tabs: {[tabId]: { - flags: {$merge: flags} - }} - }) - }, - - [TAB_UPDATE_BY_PATH]: (state, action) => { - const tabConfig = action.payload - const tabs = getTabsByPath(state, tabConfig.path) - if (!tabs.length) return state - return tabs.reduce((nextState, tab) => - update(nextState, { - tabs: {[tab.id]: { $merge: tabConfig || {} }} - }) - , state) - }, - - [TAB_MOVE_TO_GROUP]: (state, action) => { - const {tabId, groupId} = action.payload - return _moveTabToGroup(state, state.tabs[tabId], state.tabGroups[groupId]) - }, - - [TAB_INSERT_AT]: (state, action) => { - const { tabId, beforeTabId } = action.payload - const sourceTab = state.tabs[tabId] - const targetTabGroup = getTabGroupOfTab(state, state.tabs[beforeTabId]) - const insertIndex = targetTabGroup.tabIds.indexOf(beforeTabId) - return _moveTabToGroup(state, sourceTab, targetTabGroup, insertIndex) - }, - - 'PANE_CLOSE': (state, action) => { - const { targetTabGroupId, sourceTabGroupId } = action.payload - if (!targetTabGroupId) return update(state, {tabGroups: {$delete: sourceTabGroupId}}) - return _mergeTabGroups(state, targetTabGroupId, sourceTabGroupId) - }, - - [TAB_CONTEXT_MENU_OPEN]: (state, action) => ( - update(state, { - contextMenu: { $merge: action.payload } - }) - ), - - [TAB_CONTEXT_MENU_CLOSE]: (state, action) => ( - update(state, { - contextMenu: { $merge: { - isActive: false - } } - }) - ), -}, defaultState) - - -export default TabReducer - -export const TabCrossReducer = handleActions({ - [TAB_MOVE_TO_PANE]: (allState, { payload: { tabId, paneId }}) => { - const { PaneState, TabState } = allState - const pane = PaneState.panes[paneId] - const tabGroup = TabState.tabGroups[pane.content.id] - const tab = TabState.tabs[tabId] - - return { - ...allState, - TabState: _moveTabToGroup(TabState, tab, tabGroup) - } - } -}) diff --git a/app/components/Tab/selectors.js b/app/components/Tab/selectors.js deleted file mode 100644 index 26205473..00000000 --- a/app/components/Tab/selectors.js +++ /dev/null @@ -1,35 +0,0 @@ -export const getTabGroupOfTab = (state, tab) => state.tabGroups[tab.tabGroupId] - -export const getNextSiblingOfTab = (state, tab) => { - const tabIds = getTabGroupOfTab(state, tab).tabIds - if (tabIds.length === 1) return tab - let nextTabId = tabIds[tabIds.indexOf(tab.id) + 1] - if (nextTabId === undefined) nextTabId = tabIds[tabIds.indexOf(tab.id) - 1] - return state.tabs[nextTabId] -} - -export const getActiveTabGroup = (state) => state.tabGroups[state.activeTabGroupId] - -export const getActiveTabOfTabGroup = (state, tabGroup) => state.tabs[tabGroup.activeTabId] - -export const getActiveTab = (state) => { - let activeTabGroup = getActiveTabGroup(state) - if (activeTabGroup) return getActiveTabOfTabGroup(state, activeTabGroup) - return null -} - -export const isActive = (state, tabOrTabGroup) => { - let tab, tabGroup - if (typeof tabOrTabGroup.tabGroupId === 'string') { // is a tab - tab = tabOrTabGroup - tabGroup = state.tabGroups[tab.tabGroupId] - return tabGroup.activeTabId === tab.id - } else { // is a tabGroup - tabGroup = tabOrTabGroup - return state.activeTabGroupId === tabGroup.id - } -} - -export const getTabsByPath = (state, path) => { - return Object.values(state.tabs).filter(tab => tab.path === path) -} diff --git a/app/components/Terminal/index.jsx b/app/components/Terminal/Terminal.jsx similarity index 88% rename from app/components/Terminal/index.jsx rename to app/components/Terminal/Terminal.jsx index 403754ff..3ad7005f 100644 --- a/app/components/Terminal/index.jsx +++ b/app/components/Terminal/Terminal.jsx @@ -3,11 +3,10 @@ import React, { Component, PropTypes } from 'react'; import Terminal from 'sh.js'; import _ from 'lodash'; -import { connect } from 'react-redux'; import { emitter, E } from 'utils' import terms from './terminal-client'; -import * as TabActions from '../Tab/actions'; +import * as TabActions from 'commons/Tab/actions'; terms.setActions(TabActions); class Term extends Component { @@ -40,7 +39,7 @@ class Term extends Component { terms.getSocket().emit('term.input', {id: terminal.name, input: data}) }); terminal.on('title', _.debounce(title => { - _this.props.handleTabTitle(_this.props.tab.id, title) // change tab title. + _this.props.tab.title = title }, 300)); } @@ -84,11 +83,4 @@ class Term extends Component { } } - -Term = connect(null, dispatch => { - return { - handleTabTitle: (id, title) => dispatch(TabActions.updateTab({title, id})) - } -})(Term) - export default Term; diff --git a/app/components/Terminal/TerminalContainer.jsx b/app/components/Terminal/TerminalContainer.jsx new file mode 100644 index 00000000..fce39166 --- /dev/null +++ b/app/components/Terminal/TerminalContainer.jsx @@ -0,0 +1,50 @@ +import React, { Component, PropTypes } from 'react' +import cx from 'classnames' +import { TabBar, TabContent, TabContentItem, TabStateScope } from 'commons/Tab' +import Terminal from './Terminal' +import { observer } from 'mobx-react' + +const { Tab, TabGroup } = TabStateScope() + +const contextMenuItems = [ + { + name: 'Close', + icon: '', + command: 'tab:close' + }, { + name: 'Close Others', + icon: '', + command: 'tab:close_other' + }, { + name: 'Close All', + icon: '', + command: 'tab:close_all' + }, +] + +@observer +class TerminalContainer extends Component { + constructor (props) { + super(props) + const tab = new Tab() + this.tabGroup = new TabGroup() + this.tabGroup.addTab(tab) + } + + render () { + return ( +
    + + + {this.tabGroup.tabs.map(tab => + + + + )} + +
    + ) + } +} + +export default TerminalContainer diff --git a/app/components/Terminal/index.js b/app/components/Terminal/index.js new file mode 100644 index 00000000..0170e246 --- /dev/null +++ b/app/components/Terminal/index.js @@ -0,0 +1 @@ +export default from './TerminalContainer' diff --git a/app/containers/Root/index.jsx b/app/containers/Root/index.jsx index b515a4a6..58fcd272 100644 --- a/app/containers/Root/index.jsx +++ b/app/containers/Root/index.jsx @@ -1,8 +1,10 @@ /* @flow weak */ import React, { Component, PropTypes } from 'react' import { Provider, connect } from 'react-redux' +import { Provider as MobxProvider } from 'mobx-react' import store from '../../store' // initLifecycle_1: gives the defaultState +import mobxStore from '../../mobxStore' import IDE from '../IDE' import { initState } from './actions' @@ -23,7 +25,9 @@ Root = connect(null)(Root) export default () => { return ( + + ) } diff --git a/app/mobxStore.js b/app/mobxStore.js new file mode 100644 index 00000000..da6a5034 --- /dev/null +++ b/app/mobxStore.js @@ -0,0 +1,20 @@ +import { autorun, createTransformer, toJS } from 'mobx' +import PaneState from './components/Pane/state' +import EditorTabState from './components/Editor/state' +const store = { + PaneState, + EditorTabState +} + +const transform = createTransformer(store => { + return { + PaneState: toJS(store.PaneState), + EditorTabState: toJS(store.EditorTabState), + } +}) +autorun(_ => { + let transformedStore = transform(store) + console.log('[mobx store] ', transformedStore) +}) + +export default store diff --git a/app/store.js b/app/store.js index 1b7d2349..5a60903f 100644 --- a/app/store.js +++ b/app/store.js @@ -1,18 +1,19 @@ /* @flow weak */ import { createStore, combineReducers, applyMiddleware, compose } from 'redux' import { composeReducers } from './utils' +import { dispatch as emitterDispatch, emitterMiddleware } from 'utils/actions' import thunkMiddleware from 'redux-thunk' import MarkdownEditorReducer from './components/MarkdownEditor/reducer' import PanelReducer from './components/Panel/reducer' import PaneReducer, { PaneCrossReducer } from './components/Pane/reducer' -import TabReducer, { TabCrossReducer } from './components/Tab/reducer' +// import TabReducer, { TabCrossReducer } from './components/Tab/reducer' +import EditorTabReducer from './components/Editor/reducer' import FileTreeReducer from './components/FileTree/reducer' import ModalsReducer from './components/Modal/reducer' import NotificationReducer from './components/Notification/reducer' import TerminalReducer from './components/Terminal/reducer' import GitReducer from './components/Git/reducer' -import DragAndDropReducer from './components/DragAndDrop/reducer' import SettingReducer from './components/Setting/reducer' import RootReducer from './containers/Root/reducer' import PackageReducer, { PackageCrossReducer } from './components/Package/reducer' @@ -26,17 +27,17 @@ const combinedReducers = combineReducers({ FileTreeState: FileTreeReducer, PanelState: PanelReducer, PaneState: PaneReducer, - TabState: TabReducer, + // TabState: TabReducer, + EditorTabState: EditorTabReducer, ModalState: ModalsReducer, TerminalState: TerminalReducer, GitState: GitReducer, NotificationState: NotificationReducer, - DragAndDrop: DragAndDropReducer, SettingState: SettingReducer, StatusBarState: StatusBarReducer, }) -const crossReducers = composeReducers(RootReducer, PaneCrossReducer, TabCrossReducer, PackageCrossReducer) +const crossReducers = composeReducers(RootReducer, PaneCrossReducer, PackageCrossReducer) const finalReducer = composeReducers( localStoreCache.afterReducer, crossReducers, @@ -45,7 +46,7 @@ const finalReducer = composeReducers( ) const enhancer = compose( - applyMiddleware(thunkMiddleware), + applyMiddleware(thunkMiddleware, emitterMiddleware), window.devToolsExtension ? window.devToolsExtension({ serialize: { replacer: (key, value) => { diff --git a/app/styles/core-ui/DragAndDrop.styl b/app/styles/core-ui/DragAndDrop.styl index 566615ef..00d6f993 100644 --- a/app/styles/core-ui/DragAndDrop.styl +++ b/app/styles/core-ui/DragAndDrop.styl @@ -2,5 +2,5 @@ z-index: z(pane-layout-overlay); transition: all ease 0.2s; opacity: 0; - background-color: white; + background-color: hsl(210, 81%, 75%); } diff --git a/app/styles/core-ui/Tab.styl b/app/styles/core-ui/Tab.styl index d9876a6c..3d4e7b24 100644 --- a/app/styles/core-ui/Tab.styl +++ b/app/styles/core-ui/Tab.styl @@ -12,15 +12,9 @@ $tab-control-width = $tab-height*0.5; flex: 1 0 auto; position: relative; } -.tab-content-placeholder-monkey { - width: 100%; - height: 100%; - opacity: 0.3; - background: url("https://dn-coding-net-production-static.qbox.me/static/cb75498a0ffd36e2c694da62f1bdb86c.svg") no-repeat center -} .tab-content-container { absolute(0); - .tab-content-placeholding-monkey { + .tab-content-placeholder-monkey { content: ''; absolute(0); codingMonkeyBackground(); @@ -50,6 +44,7 @@ $tab-control-width = $tab-height*0.5; > .tab-show-list { width: $tab-height - 4px; + position: relative; text-align: center; flex-shrink: 0; // https://www.w3.org/TR/css-flexbox-1/#auto-margins diff --git a/app/utils/actions/createAction.js b/app/utils/actions/createAction.js new file mode 100644 index 00000000..24e0e33f --- /dev/null +++ b/app/utils/actions/createAction.js @@ -0,0 +1,34 @@ +import dispatch from './dispatch' + +const actionCreatorFactory = (genActionData) => { + return function createAction (eventName, actionPayloadCreator) { + function action (...args) { + const payload = actionPayloadCreator(...args) + const actionData = genActionData(eventName, payload) + dispatch(actionData) + return actionData + } + action.toString = () => eventName + return action + } +} + +const createAction = actionCreatorFactory( + (eventName, payload) => ({ type: eventName, payload }) +) + +export default createAction + +createAction.promise = actionCreatorFactory((eventName, payload) => { + let resolve, reject + const promise = new Promise((rs, rj) => { resolve = rs; reject = rj }) + const meta = { promise, resolve, reject } + const actionData = { + type: eventName, + payload, + meta, + then: promise.then.bind(promise), + catch: promise.catch.bind(promise), + } + return actionData +}) diff --git a/app/utils/actions/dispatch.js b/app/utils/actions/dispatch.js new file mode 100644 index 00000000..7918d25a --- /dev/null +++ b/app/utils/actions/dispatch.js @@ -0,0 +1,45 @@ +import emitter from '../emitter' + +function isValidActionObj (actionObj) { + if (!actionObj) return false + if (typeof actionObj !== 'object') return false + if (typeof actionObj.type !== 'string' || !actionObj.type) return false + return true +} + +function compose (...funcs) { + if (funcs.length === 0) return arg => arg + if (funcs.length === 1) return funcs[0] + return funcs.reduce((a, b) => (...args) => a(b(...args))) +} + +function naiveDispatch (actionObj) { + if (isValidActionObj(actionObj)) { + if (!actionObj.isDispatched) { + emitter.emit(actionObj.type, actionObj) + actionObj.isDispatched = true + } + } else if (__DEV__) { + console.warn('Invalid action object is dispatched', actionObj) + } + return actionObj +} + +let _dispatch = naiveDispatch +export function dispatch (actionObj) { + return _dispatch(actionObj) +} + +dispatch.middlewares = [] + +dispatch.applyMiddleware = function (...middlewares) { + dispatch.middlewares = dispatch.middlewares.concat(middlewares) + const middlewareAPI = { + getState: () => dispatch.state, + dispatch: action => _dispatch(action) + } + const chain = middlewares.map(middleware => middleware(middlewareAPI)) + _dispatch = compose(...chain)(_dispatch) +} + +export default dispatch diff --git a/app/utils/actions/emitterMiddleware.js b/app/utils/actions/emitterMiddleware.js new file mode 100644 index 00000000..bad243fb --- /dev/null +++ b/app/utils/actions/emitterMiddleware.js @@ -0,0 +1,7 @@ +import emitterDispatch from './dispatch' +export default function emitterMiddleware ({ dispatch, getState }) { + return next => action => { + emitterDispatch(action) + return next(action) + } +} diff --git a/app/utils/actions/handleActions.js b/app/utils/actions/handleActions.js new file mode 100644 index 00000000..8bbf1984 --- /dev/null +++ b/app/utils/actions/handleActions.js @@ -0,0 +1,12 @@ +import emitter from '../emitter' +import { action } from 'mobx' + +export default function handleActions (handlers, state) { + const eventNames = Object.keys(handlers) + eventNames.forEach(eventName => { + const handler = action(eventName, handlers[eventName]) + emitter.on(eventName, actionObj => { + return handler(state, actionObj.payload, actionObj) + }) + }) +} diff --git a/app/utils/actions/index.js b/app/utils/actions/index.js new file mode 100644 index 00000000..8bcf5b04 --- /dev/null +++ b/app/utils/actions/index.js @@ -0,0 +1,6 @@ +import dispatch from './dispatch' +import handleActions from './handleActions' +import createAction from './createAction' +import emitterMiddleware from './emitterMiddleware' + +export { dispatch, handleActions, createAction, emitterMiddleware } diff --git a/app/utils/decorators/defaultProps.js b/app/utils/decorators/defaultProps.js new file mode 100644 index 00000000..e43c0abd --- /dev/null +++ b/app/utils/decorators/defaultProps.js @@ -0,0 +1,22 @@ +import React from 'react' + +function getDisplayName (WrappedComponent) { + return WrappedComponent.displayName || WrappedComponent.name || 'Component'; +} + +export default function defaultProps (propsMapper) { + + return function decorator (WrappedComponent) { + const displayName = getDisplayName(WrappedComponent) + + function WrapperComponent (props) { + const mergedProps = { ...propsMapper(props), ...props } + return React.createElement(WrappedComponent, mergedProps) + } + + WrapperComponent.displayName = displayName + WrapperComponent.WrappedComponent = WrappedComponent + return WrapperComponent + } +} + diff --git a/app/utils/decorators/index.js b/app/utils/decorators/index.js new file mode 100644 index 00000000..d2aef227 --- /dev/null +++ b/app/utils/decorators/index.js @@ -0,0 +1,4 @@ +import mapEntityFactory from './mapEntity' +import defaultProps from './defaultProps' + +export { mapEntityFactory, defaultProps } diff --git a/app/utils/decorators/mapEntity.js b/app/utils/decorators/mapEntity.js new file mode 100644 index 00000000..3b46248d --- /dev/null +++ b/app/utils/decorators/mapEntity.js @@ -0,0 +1,30 @@ +const get = (obj, prop) => { + if (typeof obj.get === 'function') return obj.get(prop) + return obj[prop] +} + +function mapEntityFactory (entities) { + // this decorator simply allows getting entity by id + return function mapEntity (entityNames) { + entityNames = Array.isArray(entityNames) ? entityNames : [...arguments] + return function decorator (target, key, descriptor) { + let fn = descriptor.value + return { + ...descriptor, + value: function () { + let args = [...arguments] + args = args.map((entityId, i) => { + const entityName = entityNames[i] + if (entityName && typeof entityId === 'string') { + return get(entities[entityName], entityId) + } + return entityId + }) + return fn.apply(this, args) + } + } + } + } +} + +export default mapEntityFactory diff --git a/app/utils/dnd.js b/app/utils/dnd.js new file mode 100644 index 00000000..ef9b26d4 --- /dev/null +++ b/app/utils/dnd.js @@ -0,0 +1,55 @@ +import map from 'lodash/map' +import { observable, computed, action, autorun } from 'mobx' +import emitter from './emitter' + +export const DND_DRAG_START = 'DND_DRAG_START' +export const DND_DRAG_OVER = 'DND_DRAG_OVER' +export const DND_UPDATE_DRAG_OVER_META = 'DND_UPDATE_DRAG_OVER_META' +export const DND_DRAG_END = 'DND_DRAG_END' + +function getDroppables () { + var droppables = map(document.querySelectorAll('[data-droppable]'), (DOMNode) => { + return { + id: DOMNode.id, + DOMNode: DOMNode, + type: DOMNode.getAttribute('data-droppable'), + rect: DOMNode.getBoundingClientRect() + } + }) + return droppables +} + +const dnd = observable({ + isDragging: false, + source: { id: null, type: null }, + target: { id: null, type: null }, + droppables: observable.shallowArray([]), + meta: null, +}) + +dnd.dragStart = source => emitter.emit(DND_DRAG_START, source) +dnd.dragOver = target => emitter.emit(DND_DRAG_OVER, target) +dnd.updateDragOverMeta = meta => emitter.emit(DND_UPDATE_DRAG_OVER_META, meta) +dnd.dragEnd = () => emitter.emit(DND_DRAG_END) + + +emitter.on(DND_DRAG_START, (source={}) => { + dnd.isDragging = true + dnd.source = source + dnd.droppables = observable.shallowArray(getDroppables()) +}) + +emitter.on(DND_DRAG_OVER, (target={}) => { + if (!dnd.target || dnd.target.id !== target.id) dnd.target = target +}) + +emitter.on(DND_UPDATE_DRAG_OVER_META, (meta) => { + dnd.meta = meta +}) + +emitter.on(DND_DRAG_END, () => { + dnd.isDragging = false + dnd.source = dnd.target = {} +}) + +export default dnd diff --git a/app/utils/emitter.js b/app/utils/emitter/index.js similarity index 100% rename from app/utils/emitter.js rename to app/utils/emitter/index.js diff --git a/app/components/Tab/types.js b/app/utils/getTabType.js similarity index 87% rename from app/components/Tab/types.js rename to app/utils/getTabType.js index e46de316..460f26d5 100644 --- a/app/components/Tab/types.js +++ b/app/utils/getTabType.js @@ -1,4 +1,4 @@ -export const getTabType = (node) => { +export default function getTabType (node) { if ( /^text\/[^/]+/.test(node.contentType) || ( node.contentType === 'application/xml' diff --git a/app/utils/handleActions.js b/app/utils/handleActions.js new file mode 100644 index 00000000..8c8780b0 --- /dev/null +++ b/app/utils/handleActions.js @@ -0,0 +1,17 @@ +import { handleActions } from 'redux-actions' + +const wrapHandler = (handler) => { + return function (state, action) { + return handler(state, action.payload, action) + } +} + +export default function newHandleActions (handlers, defaultState) { + const wrappedHandlers = Object.keys(handlers).reduce( + (h, key) => { + h[key] = wrapHandler(handlers[key]) + return h + }, {}) + + return handleActions(wrappedHandlers, defaultState) +} diff --git a/app/utils/index.js b/app/utils/index.js index 0d312095..357038ac 100644 --- a/app/utils/index.js +++ b/app/utils/index.js @@ -13,4 +13,7 @@ export stepFactory from './stepFactory' export loadStyle from './loadStyle' export codingPackageJsonp from './codingPackageJsonp' export emitter, * as E from './emitter' +export handleActions from './handleActions' +export getTabType from './getTabType' +export dnd from './dnd' export getCookie from './getCookie' diff --git a/app/utils/themeManager.js b/app/utils/themeManager.js index be488e38..d1b686e4 100644 --- a/app/utils/themeManager.js +++ b/app/utils/themeManager.js @@ -18,6 +18,7 @@ export const changeTheme = (nextThemeId, force) => { } emitter.emit(E.THEME_CHANGED, nextThemeId) } + export const changeCodeTheme = (next) => { const nextTheme = next.split('/').pop() const currentThemeValue = getState().SettingState.views.tabs.THEME.items[1].value diff --git a/package.json b/package.json index 08141a4f..24597b9a 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,8 @@ "loader-utils": "^1.0.2", "lodash": "^4.14.2", "markdown": "^0.5.0", + "mobx": "^3.1.8", + "mobx-react": "^4.1.5", "moment": "^2.18.1", "octicons": "^4.3.0", "qs": "^6.4.0", diff --git a/webpack_configs/common.config.js b/webpack_configs/common.config.js index 833fa306..64760317 100644 --- a/webpack_configs/common.config.js +++ b/webpack_configs/common.config.js @@ -30,7 +30,11 @@ return { resolve: { extensions: ['*', '.js', '.jsx'], alias: { - 'utils': path.join(rootDir, 'app/utils/index.js') + 'app': path.join(rootDir, 'app'), + 'utils': path.join(rootDir, 'app/utils'), + 'config': path.join(rootDir, 'app/config.js'), + 'commons': path.join(rootDir, 'app/commons'), + 'components': path.join(rootDir, 'app/components'), } }, resolveLoader: { diff --git a/yarn.lock b/yarn.lock index e5181523..59ccb68a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2985,7 +2985,7 @@ hoek@2.x.x: version "2.16.3" resolved "http://registry.npm.taobao.org/hoek/download/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" -hoist-non-react-statics@^1.0.3: +hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "http://registry.npm.taobao.org/hoist-non-react-statics/download/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" @@ -4165,6 +4165,16 @@ mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkd dependencies: minimist "0.0.8" +mobx-react@^4.1.5: + version "4.1.5" + resolved "http://registry.npm.taobao.org/mobx-react/download/mobx-react-4.1.5.tgz#75cf4dbffc91b9cb23d56c060dfd8d2ca52450dc" + dependencies: + hoist-non-react-statics "^1.2.0" + +mobx@^3.1.8: + version "3.1.8" + resolved "http://registry.npm.taobao.org/mobx/download/mobx-3.1.8.tgz#94a24ce41baa9d0ccf3bd71b17dbe093bb0e1b05" + moment@^2.18.1: version "2.18.1" resolved "http://registry.npm.taobao.org/moment/download/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"