diff --git a/packages/xod-client-browser/benchmark/index.js b/packages/xod-client-browser/benchmark/index.js index 79289a15..341a478c 100644 --- a/packages/xod-client-browser/benchmark/index.js +++ b/packages/xod-client-browser/benchmark/index.js @@ -36,12 +36,12 @@ const height = 850; // open a patch group in project browser const libname = 'xod/core'; - await page.click(`.patch-group-trigger[title="${libname}"]`); + await page.click(`.patch-group-trigger[data-id="${libname}"]`); await page.waitFor(400); // wait for an animation to finish const nodeToAdd = 'add'; // select a patch in project browser - await page.click(`.PatchGroupItem[title="${nodeToAdd}"] .PatchGroupItem__label`); + await page.click(`.PatchGroupItem[data-id="${nodeToAdd}"] .PatchGroupItem__label`); await page.waitFor('.PatchGroupItem.isSelected'); // placing nodes @@ -61,8 +61,12 @@ const height = 850; } for (let row = ROWS; row >= 0; row -= 1) { - // press 'add node' button - await page.click('.PatchGroupItem.isSelected .add-node'); + // open contextmenu + await page.click('.PatchGroupItem.isSelected .contextmenu', { button: 'right' }); + await page.waitFor(100); // wait for fade In of contextmenu + await page.waitForSelector('.ContextMenu--PatchGroupItem'); + // press 'Place' button + await page.click('.ContextMenu--PatchGroupItem .react-contextmenu-item[data-id="place"]'); await page.waitForSelector('.Node.is-selected'); const nodeElementHandle = await page.$('.Node.is-selected'); diff --git a/packages/xod-client-electron/test-func/1-blink.spec.js b/packages/xod-client-electron/test-func/1-blink.spec.js index 97cdf95d..dab0c4a7 100644 --- a/packages/xod-client-electron/test-func/1-blink.spec.js +++ b/packages/xod-client-electron/test-func/1-blink.spec.js @@ -88,7 +88,7 @@ describe('IDE: Blink project', () => { it('drags a node in place', () => assert.eventually.deepEqual( ide.page.dragNode('clock', 150, 10).getLocation(), - { x: 387, y: 81 } + { x: 389, y: 81 } ) ); diff --git a/packages/xod-client-electron/test-func/pageObject.js b/packages/xod-client-electron/test-func/pageObject.js index 20cd6c33..1143c835 100644 --- a/packages/xod-client-electron/test-func/pageObject.js +++ b/packages/xod-client-electron/test-func/pageObject.js @@ -65,7 +65,7 @@ function closePopup(client) { //----------------------------------------------------------------------------- // Project browser //----------------------------------------------------------------------------- -const getSelectorForPatchInProjectBrowser = nodeName => `.PatchGroupItem[title="${nodeName}"]`; +const getSelectorForPatchInProjectBrowser = nodeName => `.PatchGroupItem[data-id="${nodeName}"]`; function findProjectBrowser(client) { return client.element('.ProjectBrowser'); @@ -81,6 +81,12 @@ function findPatchGroup(client, groupTitle) { .then(() => client.element(selector)); } +function findPatchGroupItem(client, name) { + const selector = getSelectorForPatchInProjectBrowser(name); + return findProjectBrowser(client).waitForExist(selector, 5000) + .then(() => client.element(selector)); +} + function assertPatchGroupCollapsed(client, groupTitle) { return assert.eventually.include( findPatchGroup(client, groupTitle).getAttribute('class'), @@ -114,26 +120,42 @@ function assertNodeUnavailableInProjectBrowser(client, nodeName) { function scrollToPatchInProjectBrowser(client, name) { return scrollTo( client, - '.PatchTypeSelector .inner-container', + '.ProjectBrowser', getSelectorForPatchInProjectBrowser(name) ); } function selectPatchInProjectBrowser(client, name) { - const patch = client.element(getSelectorForPatchInProjectBrowser(name)); + const patch = findPatchGroupItem(client, name); return hasClass('isSelected', patch).then( - selected => ( - selected ? Promise.resolve() : client.click(getSelectorForPatchInProjectBrowser(name)) + selected => (selected + ? Promise.resolve() + : client.click(getSelectorForPatchInProjectBrowser(name)) ) ); } +function openProjectBrowserPatchContextMenu(client, name) { + const selector = getSelectorForPatchInProjectBrowser(name); + + return selectPatchInProjectBrowser(client, name) + .then(() => client.waitForVisible(`${selector} .contextmenu`)) + .then(() => findPatchGroupItem(client, name).rightClick('.contextmenu')); +} + +function findProjectBrowserPatchContextMenu(client) { + return client.element('.ContextMenu--PatchGroupItem'); +} + function openPatchFromProjectBrowser(client, name) { return client.doubleClick(getSelectorForPatchInProjectBrowser(name)); } function clickDeletePatchButton(client, name) { - return client.click(`${getSelectorForPatchInProjectBrowser(name)} span[title="Delete patch"]`); + return openProjectBrowserPatchContextMenu(client, name) + .then( + () => findProjectBrowserPatchContextMenu(client).click('.react-contextmenu-item[data-id="delete"]') + ); } function assertPatchSelected(client, name) { @@ -144,7 +166,10 @@ function assertPatchSelected(client, name) { } function clickAddNodeButton(client, name) { - return client.element(`${getSelectorForPatchInProjectBrowser(name)} .add-node`).click(); + return openProjectBrowserPatchContextMenu(client, name) + .then( + () => findProjectBrowserPatchContextMenu(client).click('.react-contextmenu-item[data-id="place"]') + ); } function expandPatchGroup(client, groupTitle) { @@ -234,7 +259,6 @@ function addNode(client, type, dragX, dragY) { function deletePatch(client, type) { return client.waitForVisible(getSelectorForPatchInProjectBrowser(type)) .then(() => scrollToPatchInProjectBrowser(client, type)) - .then(() => selectPatchInProjectBrowser(client, type)) .then(() => clickDeletePatchButton(client, type)) .then(() => findProjectBrowser(client).click('.PopupConfirm button.Button--primary')); } @@ -329,6 +353,7 @@ const API = { findLink, findNode, findPatchGroup, + findPatchGroupItem, findPin, findPopup, getCodeboxValue, @@ -336,6 +361,7 @@ const API = { scrollToPatchInProjectBrowser, selectPatchInProjectBrowser, openPatchFromProjectBrowser, + openProjectBrowserPatchContextMenu, deletePatch, expandPatchGroup, // LibSuggester diff --git a/packages/xod-client/package.json b/packages/xod-client/package.json index 47027898..f72d569d 100644 --- a/packages/xod-client/package.json +++ b/packages/xod-client/package.json @@ -39,6 +39,7 @@ "react-click-outside": "^2.2.0", "react-codemirror": "^1.0.0", "react-collapsible": "^2.0.3", + "react-contextmenu": "^2.9.1", "react-custom-scroll": "^2.0.0", "react-dnd": "^2.5.1", "react-dnd-html5-backend": "^2.5.1", diff --git a/packages/xod-client/src/core/assets/icons/addlib.svg b/packages/xod-client/src/core/assets/icons/addlib.svg new file mode 100644 index 00000000..20f0e47b --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/addlib.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/xod-client/src/core/assets/icons/contextmenu.svg b/packages/xod-client/src/core/assets/icons/contextmenu.svg new file mode 100644 index 00000000..f8b24fa7 --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/contextmenu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/xod-client/src/core/assets/icons/libs.svg b/packages/xod-client/src/core/assets/icons/libs.svg new file mode 100644 index 00000000..a6342eda --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/libs.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/xod-client/src/core/assets/icons/newpatch.svg b/packages/xod-client/src/core/assets/icons/newpatch.svg new file mode 100644 index 00000000..644a5559 --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/newpatch.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/xod-client/src/core/assets/icons/project.svg b/packages/xod-client/src/core/assets/icons/project.svg new file mode 100644 index 00000000..e44e028d --- /dev/null +++ b/packages/xod-client/src/core/assets/icons/project.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/packages/xod-client/src/core/styles/abstracts/colors.scss b/packages/xod-client/src/core/styles/abstracts/colors.scss index d4472d87..e90528bb 100644 --- a/packages/xod-client/src/core/styles/abstracts/colors.scss +++ b/packages/xod-client/src/core/styles/abstracts/colors.scss @@ -67,7 +67,7 @@ $selected: $cyan; $error: $red; $chrome-bg: $dark; -$chrome-title: $coal-bright; +$chrome-title-bg: $coal-bright; $chrome-title-button-hover: $dark; $chrome-outlines: $coal; diff --git a/packages/xod-client/src/core/styles/abstracts/icons.scss b/packages/xod-client/src/core/styles/abstracts/icons.scss new file mode 100644 index 00000000..930b201d --- /dev/null +++ b/packages/xod-client/src/core/styles/abstracts/icons.scss @@ -0,0 +1,21 @@ +@mixin icon($url) { + &:before { + content: ''; + display: block; + line-height: 0; + + width: 100%; + height: 100%; + background: url($url) no-repeat center center; + } +} + +.icon-newpatch { + @include icon('../assets/icons/newpatch.svg'); +} +.icon-addlib { + @include icon('../assets/icons/addlib.svg'); +} +.icon-contextmenu { + @include icon('../assets/icons/contextmenu.svg'); +} diff --git a/packages/xod-client/src/core/styles/abstracts/variables.scss b/packages/xod-client/src/core/styles/abstracts/variables.scss index 505de756..f7d9b38e 100644 --- a/packages/xod-client/src/core/styles/abstracts/variables.scss +++ b/packages/xod-client/src/core/styles/abstracts/variables.scss @@ -59,19 +59,14 @@ $color-tabs-debugger-text: #82b948; $sidebar-width: 200px; -$sidebar-color-bg: #3d3d3d; -$sidebar-color-bg-even: #444444; -$sidebar-color-bg-hover: #5f5f5f; -$sidebar-color-bg-selected: #616077; - -$sidebar-color-text: #cdcbc7; -$sidebar-color-text-hover: white; - -$sidebar-color-border: #595959; - -$sidebar-color-bg-tabs: #373737; -$sidebar-color-bg-tabs-selected: #cccccc; -$sidebar-color-text-selected-tab: #000; +$sidebar-color-bg: $chrome-bg; +$sidebar-color-bg-even: $chrome-bg; +$sidebar-color-bg-hover: $grey; +$sidebar-color-bg-selected: $blackberry; +$sidebar-color-bg-selected-hover: $blackberry-light; +$sidebar-color-text: $chalk; +$sidebar-color-text-hover: $chalk; +$sidebar-color-border: $chrome-outlines; $input-color-text: #cccccc; $input-color-bg: #373737; diff --git a/packages/xod-client/src/core/styles/components/Breadcrumbs.scss b/packages/xod-client/src/core/styles/components/Breadcrumbs.scss index 848ddc11..96cbcef3 100644 --- a/packages/xod-client/src/core/styles/components/Breadcrumbs.scss +++ b/packages/xod-client/src/core/styles/components/Breadcrumbs.scss @@ -49,7 +49,7 @@ display: inline-block; background: $sidebar-color-bg-even; border: none; - color: $sidebar-color-bg-tabs-selected; + color: $chalk; outline: none; height: 24px; diff --git a/packages/xod-client/src/core/styles/components/ContextMenu.scss b/packages/xod-client/src/core/styles/components/ContextMenu.scss new file mode 100644 index 00000000..5edc6a0f --- /dev/null +++ b/packages/xod-client/src/core/styles/components/ContextMenu.scss @@ -0,0 +1,84 @@ +.ContextMenu { + &--hide { + display: none; + } +} + +.react-contextmenu { + min-width: 140px; + padding: 0; + margin: 2px 0 0; + + font-family: $font-family-normal; + font-size: $font-size-m; + color: $chalk; + text-align: left; + + background-color: $chrome-bg; + background-clip: border-box; + border: 1px solid $chrome-outlines; + box-shadow: 5px 5px 10px -5px rgba(0,0,0,.5); + outline: none; + + opacity: 0; + pointer-events: none; + transition: opacity 100ms ease !important; + + user-select: none; + + z-index: 1000; + + &--visible { + opacity: 1; + pointer-events: auto; + } + + &-item { + padding: 6px 12px; + line-height: 1; + color: $chalk; + text-align: inherit; + white-space: nowrap; + background: transparent; + border: 0; + cursor: pointer; + outline: 0; + + .accelerator { + float: right; + color: $light-grey; + } + + &--active, &--selected, &:active { + color: $chalk; + background-color: $blackberry; + text-decoration: none; + } + + &--disabled, &--disabled:hover { + color: $light-grey; + background: transparent; + cursor: default; + } + + &--divider, &--divider:hover { + background: transparent; + padding: 0; + margin: 3px 0; + height: 1px; + background-color: $coal-bright; + cursor: default; + } + } + + &-sumbenu { + padding: 0; + } + &-submenu > &-item:after { + content: "▶"; + font-size: $font-size-s; + display: inline-block; + position: absolute; + right: 6px; + } +} diff --git a/packages/xod-client/src/core/styles/components/PatchGroup.scss b/packages/xod-client/src/core/styles/components/PatchGroup.scss index a554d290..f80269a0 100644 --- a/packages/xod-client/src/core/styles/components/PatchGroup.scss +++ b/packages/xod-client/src/core/styles/components/PatchGroup.scss @@ -6,25 +6,29 @@ font-family: $font-family-normal; text-decoration: none; position: relative; - border: 1px solid $sidebar-color-border; - border-bottom: none; - padding: 10px 10px 10px 30px; + border-top: 1px solid $sidebar-color-border; + padding: 10px 10px 9px 30px; color: $sidebar-color-text; user-select: none; + line-height: 1em; &.my:before, &.library:before { - font-family: 'FontAwesome'; - font-size: 13px; + content: ''; position: absolute; - left: 10px; - top: 10px; + left: 6px; + top: 8px; display: block; - content: '\f007'; // TODO: bg image + + width: 14px; + height: 14px; + background: url(../assets/icons/project.svg) no-repeat; } &.library:before { - content: '\f02d'; // TODO: bg image + width: 14px; + height: 14px; + background: url(../assets/icons/libs.svg) no-repeat; } } diff --git a/packages/xod-client/src/core/styles/components/PatchGroupItem.scss b/packages/xod-client/src/core/styles/components/PatchGroupItem.scss index 489faa09..9ac7d052 100644 --- a/packages/xod-client/src/core/styles/components/PatchGroupItem.scss +++ b/packages/xod-client/src/core/styles/components/PatchGroupItem.scss @@ -26,7 +26,7 @@ right: 0; top: 0; box-sizing: border-box; - padding: 7px 6px 8px 1px; + padding: 0 8px 0 8px; font-size: 14px; // for FA icons &:before { @@ -39,6 +39,16 @@ left: -8px; background: linear-gradient(to left, $sidebar-color-bg-hover, rgba(0, 0, 0, 0)); } + + & > * { + margin-left: 2px; + } + + .contextmenu { + width: 30px; + height: 30px; + @extend .icon-contextmenu; + } } &:nth-child(even) { @@ -55,6 +65,17 @@ background: linear-gradient(to left, $sidebar-color-bg-selected, rgba(0, 0, 0, 0)); } } + + &:hover { + background-color: $sidebar-color-bg-selected-hover; + + .PatchGroupItem__hover-buttons { + background-color: $sidebar-color-bg-selected-hover; + &:before { + background: linear-gradient(to left, $sidebar-color-bg-selected-hover, rgba(0, 0, 0, 0)); + } + } + } } &:hover { diff --git a/packages/xod-client/src/core/styles/components/PatchTypeSelector.scss b/packages/xod-client/src/core/styles/components/PatchTypeSelector.scss deleted file mode 100644 index 75d14d21..00000000 --- a/packages/xod-client/src/core/styles/components/PatchTypeSelector.scss +++ /dev/null @@ -1,36 +0,0 @@ -.PatchTypeSelector { - height: calc(100% - 30px); // 30px is a height of .ProjectBrowserToolbar - - .options { - display: flex; - list-style: none; - margin: 0; - padding: 0; - } - - .option { - flex: 1; - margin: 0; - padding: 9px; - text-align: center; - cursor: pointer; - background-color: $sidebar-color-bg-tabs; - color: $sidebar-color-text; - font-family: $font-family-normal; - font-size: $font-size-s; - user-select: none; - border: 1px solid $sidebar-color-border; - border-left: 0; - white-space: nowrap; - - &:first-child { - border-left: 1px solid $sidebar-color-border; - } - - &.selected { - background-color: $sidebar-color-bg-tabs-selected; - color: $sidebar-color-text-selected-tab; - cursor: default; - } - } -} diff --git a/packages/xod-client/src/core/styles/components/ProjectBrowserToolbar.scss b/packages/xod-client/src/core/styles/components/ProjectBrowserToolbar.scss index 62ed36e7..fdfc0338 100644 --- a/packages/xod-client/src/core/styles/components/ProjectBrowserToolbar.scss +++ b/packages/xod-client/src/core/styles/components/ProjectBrowserToolbar.scss @@ -6,7 +6,7 @@ font-size: $font-size-l; padding: 3px; - background-color: $sidebar-color-bg; + background-color: $chrome-title-bg; button { display: inline-block; @@ -30,12 +30,26 @@ &:hover, &:focus { color: $sidebar-color-text-hover; } + + &.addlib{ + @extend .icon-addlib; + } + &.newpatch{ + @extend .icon-newpatch; + } + &.contextmenu { + @extend .icon-contextmenu; + } } - &-left { + &-title { + font-size: $font-size-m; + color: $light-grey-bright; float: left; + line-height: 26px; + padding-left: 3px; } - &-right { + &-buttons { float: right; } -} \ No newline at end of file +} diff --git a/packages/xod-client/src/core/styles/components/Sidebar.scss b/packages/xod-client/src/core/styles/components/Sidebar.scss index f6596ce0..8c68c57e 100644 --- a/packages/xod-client/src/core/styles/components/Sidebar.scss +++ b/packages/xod-client/src/core/styles/components/Sidebar.scss @@ -7,6 +7,7 @@ flex-wrap: nowrap; background: $sidebar-color-bg; + border-right: 2px solid $chrome-outlines; .title { display: block; diff --git a/packages/xod-client/src/core/styles/components/SplitPane.scss b/packages/xod-client/src/core/styles/components/SplitPane.scss index 0c76d2a7..7acb9ffb 100644 --- a/packages/xod-client/src/core/styles/components/SplitPane.scss +++ b/packages/xod-client/src/core/styles/components/SplitPane.scss @@ -8,7 +8,7 @@ content: ''; display: block; width: 100%; - height: 1px; + height: 2px; background: $sidebar-color-border; } @@ -19,9 +19,9 @@ cursor: row-resize; &:hover { - background: rgba($sidebar-color-border, 0.4); + background: $dark-light; &:before { - background: lighten($sidebar-color-border, 10); + background: $sidebar-color-border; } } } diff --git a/packages/xod-client/src/core/styles/components/Workarea.scss b/packages/xod-client/src/core/styles/components/Workarea.scss index adcf5257..cf227563 100644 --- a/packages/xod-client/src/core/styles/components/Workarea.scss +++ b/packages/xod-client/src/core/styles/components/Workarea.scss @@ -1,10 +1,6 @@ -.Workarea-container { - flex-grow: 1; -} - .Workarea { position: relative; - height: 100%; display: flex; flex-direction: column; + flex-grow: 1; } diff --git a/packages/xod-client/src/core/styles/main.scss b/packages/xod-client/src/core/styles/main.scss index 827028f3..45d0a4c5 100644 --- a/packages/xod-client/src/core/styles/main.scss +++ b/packages/xod-client/src/core/styles/main.scss @@ -1,6 +1,7 @@ @import 'abstracts/colors', 'abstracts/variables', + 'abstracts/icons', 'abstracts/functions', 'abstracts/mixins'; @@ -21,6 +22,7 @@ 'components/Button', 'components/CloseButton', 'components/Comment', + 'components/ContextMenu', 'components/CppImplementationEditor', 'components/Debugger', 'components/Editor', @@ -36,7 +38,6 @@ 'components/PatchGroup', 'components/PatchGroupItem', 'components/PatchSVG', - 'components/PatchTypeSelector', 'components/PatchWrapper', 'components/Pin', 'components/PinLabel', diff --git a/packages/xod-client/src/editor/actionTypes.js b/packages/xod-client/src/editor/actionTypes.js index 522c2482..a05c0f51 100644 --- a/packages/xod-client/src/editor/actionTypes.js +++ b/packages/xod-client/src/editor/actionTypes.js @@ -20,6 +20,8 @@ export const PASTE_ENTITIES = 'PASTE_ENTITIES'; export const TAB_CLOSE = 'TAB_CLOSE'; export const TAB_SORT = 'TAB_SORT'; +export const SHOW_HELPBAR = 'SHOW_HELPBAR'; +export const HIDE_HELPBAR = 'HIDE_HELPBAR'; export const TOGGLE_HELPBAR = 'TOGGLE_HELPBAR'; export const SHOW_SUGGESTER = 'SHOW_SUGGESTER'; diff --git a/packages/xod-client/src/editor/actions.js b/packages/xod-client/src/editor/actions.js index 71d72098..d802c9e2 100644 --- a/packages/xod-client/src/editor/actions.js +++ b/packages/xod-client/src/editor/actions.js @@ -262,6 +262,12 @@ export const sortTabs = newOrderObject => ({ payload: newOrderObject, }); +export const hideHelpbar = () => ({ + type: ActionType.HIDE_HELPBAR, +}); +export const showHelpbar = () => ({ + type: ActionType.SHOW_HELPBAR, +}); export const toggleHelpbar = () => ({ type: ActionType.TOGGLE_HELPBAR, }); diff --git a/packages/xod-client/src/editor/components/LibSuggester.jsx b/packages/xod-client/src/editor/components/LibSuggester.jsx index e80246de..ddc83cae 100644 --- a/packages/xod-client/src/editor/components/LibSuggester.jsx +++ b/packages/xod-client/src/editor/components/LibSuggester.jsx @@ -58,7 +58,10 @@ class LibSuggester extends React.Component { componentDidMount() { if (this.input) { // A hack to avoid typing any character into input when pressing Hotkey - setTimeout(() => this.input.focus(), 1); + setTimeout(() => { + this.input.focus(); + this.props.onInitialFocus(); + }, 1); } } @@ -234,12 +237,14 @@ class LibSuggester extends React.Component { LibSuggester.defaultProps = { addClassName: '', onBlur: () => {}, + onInitialFocus: () => {}, }; LibSuggester.propTypes = { addClassName: PropTypes.string, onInstallLibrary: PropTypes.func.isRequired, onBlur: PropTypes.func, + onInitialFocus: PropTypes.func, }; export default LibSuggester; diff --git a/packages/xod-client/src/editor/constants.js b/packages/xod-client/src/editor/constants.js index f2b71c0f..b9ba489d 100644 --- a/packages/xod-client/src/editor/constants.js +++ b/packages/xod-client/src/editor/constants.js @@ -58,6 +58,7 @@ export const FOCUS_AREAS = { INSPECTOR: 'INSPECTOR', WORKAREA: 'WORKAREA', NODE_SUGGESTER: 'NODE_SUGGESTER', + LIB_SUGGESTER: 'LIB_SUGGESTER', }; export const CLIPBOARD_DATA_TYPE = 'text/xod-entities'; diff --git a/packages/xod-client/src/editor/containers/Editor.jsx b/packages/xod-client/src/editor/containers/Editor.jsx index b5ad26a5..1f42a051 100644 --- a/packages/xod-client/src/editor/containers/Editor.jsx +++ b/packages/xod-client/src/editor/containers/Editor.jsx @@ -32,7 +32,6 @@ import Inspector from '../components/Inspector'; import Debugger from '../../debugger/containers/Debugger'; import Breadcrumbs from '../../debugger/containers/Breadcrumbs'; import Sidebar from '../components/Sidebar'; -import Workarea from '../../utils/components/Workarea'; import SnackBar from '../../messages/containers/SnackBar'; import { RenderableSelection } from '../../types'; @@ -55,6 +54,12 @@ class Editor extends React.Component { this.showSuggester = this.showSuggester.bind(this); this.hideSuggester = this.hideSuggester.bind(this); + this.onProjectBrowserFocus = this.onProjectBrowserFocus.bind(this); + this.onInspectorFocus = this.onInspectorFocus.bind(this); + this.onWorkareaFocus = this.onWorkareaFocus.bind(this); + this.onNodeSuggesterFocus = this.onNodeSuggesterFocus.bind(this); + this.onLibSuggesterFocus = this.onLibSuggesterFocus.bind(this); + this.patchSize = this.props.size; this.updatePatchImplementationDebounced = @@ -77,6 +82,22 @@ class Editor extends React.Component { return this.props.actions.installLibraries([libParams]); } + onProjectBrowserFocus() { + this.props.actions.setFocusedArea(FOCUS_AREAS.PROJECT_BROWSER); + } + onInspectorFocus() { + this.props.actions.setFocusedArea(FOCUS_AREAS.INSPECTOR); + } + onWorkareaFocus() { + this.props.actions.setFocusedArea(FOCUS_AREAS.WORKAREA); + } + onNodeSuggesterFocus() { + this.props.actions.setFocusedArea(FOCUS_AREAS.NODE_SUGGESTER); + } + onLibSuggesterFocus() { + this.props.actions.setFocusedArea(FOCUS_AREAS.LIB_SUGGESTER); + } + getHotkeyHandlers() { return { [COMMAND.UNDO]: () => this.props.actions.undo(this.props.currentPatchPath), @@ -169,7 +190,7 @@ class Editor extends React.Component { index={patchesIndex} onAddNode={this.onAddNode} onBlur={this.hideSuggester} - onInitialFocus={() => this.props.actions.setFocusedArea(FOCUS_AREAS.NODE_SUGGESTER)} + onInitialFocus={this.onNodeSuggesterFocus} onHighlight={this.props.actions.highlightSugessterItem} /> ) : null; @@ -179,6 +200,7 @@ class Editor extends React.Component { addClassName={(this.props.isHelpbarVisible) ? 'with-helpbar' : ''} onInstallLibrary={this.onInstallLibrary} onBlur={this.props.actions.hideLibSuggester} + onInitialFocus={this.onLibSuggesterFocus} /> ) : null; @@ -207,13 +229,13 @@ class Editor extends React.Component { > this.props.actions.setFocusedArea(FOCUS_AREAS.PROJECT_BROWSER)} + onFocus={this.onProjectBrowserFocus} > this.props.actions.setFocusedArea(FOCUS_AREAS.INSPECTOR)} + onFocus={this.onInspectorFocus} > + {suggester} + {libSuggester} this.props.actions.setFocusedArea(FOCUS_AREAS.WORKAREA)} + className="Workarea" + onFocus={this.onWorkareaFocus} > - - - {DebugSessionStopButton} - {this.renderOpenedPatchTab()} - {suggester} - {libSuggester} - {BreadcrumbsContainer} - {this.renderOpenedImplementationEditorTabs()} - {DebuggerContainer} - - + + {DebugSessionStopButton} + {this.renderOpenedPatchTab()} + {BreadcrumbsContainer} + {this.renderOpenedImplementationEditorTabs()} + {DebuggerContainer} + diff --git a/packages/xod-client/src/editor/reducer.js b/packages/xod-client/src/editor/reducer.js index d891ac56..b9c0f15c 100644 --- a/packages/xod-client/src/editor/reducer.js +++ b/packages/xod-client/src/editor/reducer.js @@ -553,6 +553,10 @@ const editorReducer = (state = {}, action) => { // // helpbar // + case EAT.HIDE_HELPBAR: + return R.over(R.lensProp('isHelpbarVisible'), R.F, state); + case EAT.SHOW_HELPBAR: + return R.over(R.lensProp('isHelpbarVisible'), R.T, state); case EAT.TOGGLE_HELPBAR: return R.over(R.lensProp('isHelpbarVisible'), R.not, state); diff --git a/packages/xod-client/src/projectBrowser/components/PatchGroup.jsx b/packages/xod-client/src/projectBrowser/components/PatchGroup.jsx index d32391e0..3b34655c 100644 --- a/packages/xod-client/src/projectBrowser/components/PatchGroup.jsx +++ b/packages/xod-client/src/projectBrowser/components/PatchGroup.jsx @@ -1,14 +1,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import Collapsible from 'react-collapsible'; -import 'font-awesome/scss/font-awesome.scss'; import { noop } from '../../utils/ramda'; const PatchGroup = ({ name, children, type, onClose }) => ( {name}} + trigger={ + {name} + } triggerClassName={type} triggerOpenedClassName={type} transitionTime={100} diff --git a/packages/xod-client/src/projectBrowser/components/PatchGroupItem.jsx b/packages/xod-client/src/projectBrowser/components/PatchGroupItem.jsx index 36bc15df..1924d7e8 100644 --- a/packages/xod-client/src/projectBrowser/components/PatchGroupItem.jsx +++ b/packages/xod-client/src/projectBrowser/components/PatchGroupItem.jsx @@ -5,8 +5,9 @@ import cn from 'classnames'; import { DragSource } from 'react-dnd'; import { getEmptyImage } from 'react-dnd-html5-backend'; import { Icon } from 'react-fa'; -import 'font-awesome/scss/font-awesome.scss'; +import { ContextMenuTrigger } from 'react-contextmenu'; +import { PATCH_GROUP_CONTEXT_MENU_ID } from '../constants'; import { DRAGGED_ENTITY_TYPE } from '../../editor/constants'; const dragSource = { @@ -63,6 +64,7 @@ class PatchGroupItem extends React.Component { onClick, onDoubleClick, connectDragSource, + collectPropsFn, ...restProps } = this.props; @@ -76,24 +78,30 @@ class PatchGroupItem extends React.Component { ); return connectDragSource( -
-
- {(dead) ? deadIcon : null} - {label} -
+
+ {(dead) ? deadIcon : null} + {label} +
+
{hoverButtons} - {/* placeholder for context menu opener */} - {/* */}
); @@ -113,6 +121,7 @@ PatchGroupItem.propTypes = { connectDragSource: PropTypes.func.isRequired, connectDragPreview: PropTypes.func.isRequired, onBeginDrag: PropTypes.func.isRequired, + collectPropsFn: PropTypes.func.isRequired, }; export default DragSource( // eslint-disable-line new-cap diff --git a/packages/xod-client/src/projectBrowser/components/PatchGroupItemContextMenu.jsx b/packages/xod-client/src/projectBrowser/components/PatchGroupItemContextMenu.jsx new file mode 100644 index 00000000..0bcf1df3 --- /dev/null +++ b/packages/xod-client/src/projectBrowser/components/PatchGroupItemContextMenu.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import { ContextMenu, MenuItem, connectMenu } from 'react-contextmenu'; + +import { PATCH_GROUP_CONTEXT_MENU_ID } from '../constants'; + +const callCallbackWithPatchPath = onClick => (event, data) => onClick(data.patchPath); + +const PatchGroupItemContextMenu = (props) => { + const trigger = (props.trigger) ? props.trigger : {}; + + const renamePatch = (trigger.isLocalPatch) + ? ( + + Rename + + ) : null; + + const deletePatch = (trigger.isLocalPatch) + ? ( + + Delete + + ) : null; + + const cls = cn('ContextMenu ContextMenu--PatchGroupItem', { + // It's a hack to prevent rendering contextmenu + // after click something with wrong menu items + 'ContextMenu--hide': !props.trigger, + }); + + return ( + + + + drag&drop + + Place + + + + + click×2 + + Open + + {renamePatch} + {deletePatch} + + + + h + + Help + + + ); +}; + +PatchGroupItemContextMenu.propTypes = { + trigger: PropTypes.shape({ + /* eslint-disable react/no-unused-prop-types */ + patchPath: PropTypes.string.isRequired, + canAdd: PropTypes.bool.isRequired, + isLocalPatch: PropTypes.bool.isRequired, + /* eslint-enable react/no-unused-prop-types */ + }), + onPatchAdd: PropTypes.func.isRequired, + onPatchOpen: PropTypes.func.isRequired, + onPatchDelete: PropTypes.func.isRequired, + onPatchRename: PropTypes.func.isRequired, + onPatchHelp: PropTypes.func.isRequired, +}; + +export default connectMenu(PATCH_GROUP_CONTEXT_MENU_ID)(PatchGroupItemContextMenu); diff --git a/packages/xod-client/src/projectBrowser/components/PatchTypeSelector.jsx b/packages/xod-client/src/projectBrowser/components/PatchTypeSelector.jsx deleted file mode 100644 index f02a4b25..00000000 --- a/packages/xod-client/src/projectBrowser/components/PatchTypeSelector.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import R from 'ramda'; -import React from 'react'; -import PropTypes from 'prop-types'; -import cn from 'classnames'; - -import { noop } from '../../utils/ramda'; - -/* eslint-disable jsx-a11y/no-static-element-interactions */ - -class PatchTypeSelector extends React.PureComponent { - constructor(props) { - super(props); - - // TODO: enforse non-empty props.options? - // TODO: check that initialSelectedKey is equal to one of option's keys? - - const selectedOptionKey = R.ifElse( - R.propSatisfies(R.isNil, 'initialSelectedKey'), - R.path(['options', 0, 'key']), - R.prop('initialSelectedKey') - )(props); - - this.state = { selectedOptionKey }; - } - - onSelect(selectedOptionKey) { - if (selectedOptionKey === this.state.selectedOptionKey) { - return; - } - - this.setState({ selectedOptionKey }); - this.props.onChange(selectedOptionKey); - } - - render() { - const { options, children } = this.props; - const { selectedOptionKey } = this.state; - - return ( -
-
    - {options.map(({ key, name }) => -
  • this.onSelect(key)} - > - {name} -
  • - )} -
- {children(selectedOptionKey)} -
- ); - } -} - -PatchTypeSelector.displayName = 'PatchTypeSelector'; - -PatchTypeSelector.propTypes = { - options: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types - name: PropTypes.string.isRequired, // eslint-disable-line react/no-unused-prop-types - }) - ).isRequired, - onChange: PropTypes.func.isRequired, - children: PropTypes.func.isRequired, -}; - -PatchTypeSelector.defaultProps = { - onChange: noop, -}; - -export default PatchTypeSelector; diff --git a/packages/xod-client/src/projectBrowser/components/ProjectBrowserToolbar.jsx b/packages/xod-client/src/projectBrowser/components/ProjectBrowserToolbar.jsx index ae35ed14..d5ba8fd9 100644 --- a/packages/xod-client/src/projectBrowser/components/ProjectBrowserToolbar.jsx +++ b/packages/xod-client/src/projectBrowser/components/ProjectBrowserToolbar.jsx @@ -1,16 +1,22 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Icon } from 'react-fa'; -const ProjectBrowserToolbar = ({ onClickAddPatch }) => ( +const ProjectBrowserToolbar = ({ onClickAddPatch, onClickAddLibrary }) => (
-
+
+ Project Browser +
+
+ /> +
); @@ -19,6 +25,7 @@ ProjectBrowserToolbar.displayName = 'ProjectBrowserToolbar'; ProjectBrowserToolbar.propTypes = { onClickAddPatch: PropTypes.func.isRequired, + onClickAddLibrary: PropTypes.func.isRequired, }; export default ProjectBrowserToolbar; diff --git a/packages/xod-client/src/projectBrowser/constants.js b/packages/xod-client/src/projectBrowser/constants.js new file mode 100644 index 00000000..59738dd0 --- /dev/null +++ b/packages/xod-client/src/projectBrowser/constants.js @@ -0,0 +1,3 @@ +// Id of context menu to show it +// eslint-disable-next-line import/prefer-default-export +export const PATCH_GROUP_CONTEXT_MENU_ID = 'PATCH_GROUP_CONTEXT_MENU_ID'; diff --git a/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx b/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx index 285e865e..17deb98a 100644 --- a/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx +++ b/packages/xod-client/src/projectBrowser/containers/ProjectBrowser.jsx @@ -1,12 +1,12 @@ import R from 'ramda'; import React from 'react'; import PropTypes from 'prop-types'; -import cn from 'classnames'; import CustomScroll from 'react-custom-scroll'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Icon } from 'react-fa'; import { HotKeys } from 'react-hotkeys'; +import { ContextMenuTrigger } from 'react-contextmenu'; import $ from 'sanctuary-def'; import { @@ -28,23 +28,15 @@ import * as PopupSelectors from '../../popups/selectors'; import * as EditorSelectors from '../../editor/selectors'; import { COMMAND } from '../../utils/constants'; -import { noop } from '../../utils/ramda'; import sanctuaryPropType from '../../utils/sanctuaryPropType'; import PatchGroup from '../components/PatchGroup'; import PatchGroupItem from '../components/PatchGroupItem'; -import PatchTypeSelector from '../components/PatchTypeSelector'; import ProjectBrowserPopups from '../components/ProjectBrowserPopups'; import ProjectBrowserToolbar from '../components/ProjectBrowserToolbar'; +import PatchGroupItemContextMenu from '../components/PatchGroupItemContextMenu'; -import { getUtmSiteUrl } from '../../utils/urls'; -import { IconGuide } from '../../utils/components/IconGuide'; - -const PATCH_TYPE = { - ALL: 'all', - MY: 'my', - LIBRARY: 'library', -}; +import { PATCH_GROUP_CONTEXT_MENU_ID } from '../constants'; const pickPatchPartsForComparsion = R.map(R.pick(['dead', 'path'])); @@ -69,17 +61,21 @@ const pickPropsForComparsion = R.compose( class ProjectBrowser extends React.Component { constructor(props) { super(props); - this.patchRenderers = { - [PATCH_TYPE.MY]: this.renderLocalPatches.bind(this), - [PATCH_TYPE.LIBRARY]: this.renderLibraryPatches.bind(this), - }; + this.state = {}; this.renderPatches = this.renderPatches.bind(this); this.deselectIfInLibrary = this.deselectIfInLibrary.bind(this); this.deselectIfInLocalPatches = this.deselectIfInLocalPatches.bind(this); + this.onAddNode = this.onAddNode.bind(this); this.onRenameHotkey = this.onRenameHotkey.bind(this); this.onDeleteHotkey = this.onDeleteHotkey.bind(this); + this.onClickAddLibrary = this.onClickAddLibrary.bind(this); + this.onPatchHelpClicked = this.onPatchHelpClicked.bind(this); + + this.renderItem = this.renderItem.bind(this); + this.renderLocalPatches = this.renderLocalPatches.bind(this); + this.renderLibraryPatches = this.renderLibraryPatches.bind(this); } shouldComponentUpdate(nextProps) { @@ -112,6 +108,15 @@ class ProjectBrowser extends React.Component { this.props.actions.requestDelete(selectedPatchPath); } + onClickAddLibrary(event) { + event.stopPropagation(); + this.props.actions.showLibSuggester(); + } + + onPatchHelpClicked() { + this.props.actions.showHelpbar(); + } + getHotkeyHandlers() { return { [COMMAND.ADD_PATCH]: this.props.actions.requestCreatePatch, @@ -121,36 +126,14 @@ class ProjectBrowser extends React.Component { }; } - localPatchesHoveredButtons(patchPath) { - const { - requestRename, - requestDelete, - } = this.props.actions; - - return [ - requestDelete(patchPath)} - />, - requestRename(patchPath)} - />, - this.renderAddNodeButton(patchPath), - ]; - } - - libraryPatchesHoveredButtons(path) { - return [ - this.renderDocsButton(path), - this.renderAddNodeButton(path), - ]; + getCollectPropsFn(patchPath) { + const { currentPatchPath } = this.props; + const canAdd = currentPatchPath !== patchPath; + return () => ({ + patchPath, + canAdd, + isLocalPatch: isPathLocal(patchPath), + }); } deselectIfInLocalPatches() { @@ -167,50 +150,27 @@ class ProjectBrowser extends React.Component { }; } - renderAddNodeButton(patchPath) { - const { currentPatchPath } = this.props; - - const isCurrentPatch = currentPatchPath === patchPath; - const canAdd = !isCurrentPatch; - - const classNames = cn('hover-button add-node', { disabled: !canAdd }); - const action = canAdd ? () => this.onAddNode(patchPath) : noop; - - return ( - - ); - } - - renderDocsButton(patchPath) { // eslint-disable-line class-methods-use-this - return ( - - - - ); + {/* Component needs at least one child :-( */} + + , + ]; } - renderLocalPatches() { + renderItem({ path, dead }) { const { - projectName, - localPatches, currentPatchPath, selectedPatchPath, } = this.props; @@ -221,7 +181,9 @@ class ProjectBrowser extends React.Component { startDraggingPatch, } = this.props.actions; - const renderItem = ({ path, dead }) => ( + const collectPropsFn = this.getCollectPropsFn(path); + + return ( setSelection(path)} - hoverButtons={this.localPatchesHoveredButtons(path)} + collectPropsFn={collectPropsFn} + hoverButtons={this.renderHoveredButtons(collectPropsFn)} /> ); + } + + renderLocalPatches() { + const { + projectName, + localPatches, + } = this.props; return ( - {R.map(renderItem, localPatches)} + {localPatches.map(this.renderItem)} ); } renderLibraryPatches() { - const { libs, installingLibs, selectedPatchPath } = this.props; - const { setSelection, switchPatch, startDraggingPatch } = this.props.actions; + const { libs, installingLibs } = this.props; const installingLibsComponents = R.map( ({ owner, name, version }) => ({ name: `${owner}/${name}`, component: (
{owner}/{name} @@ -279,19 +249,7 @@ class ProjectBrowser extends React.Component { name={libName} onClose={this.deselectIfInLibrary(libName)} > - {libPatches.map(({ path, dead }) => - setSelection(path)} - onDoubleClick={() => switchPatch(path)} - onBeginDrag={startDraggingPatch} - hoverButtons={this.libraryPatchesHoveredButtons(path)} - /> - )} + {libPatches.map(this.renderItem)} ), })), R.toPairs @@ -304,16 +262,13 @@ class ProjectBrowser extends React.Component { )(libComponents, installingLibsComponents); } - renderPatches(patchType) { - const rendererKeys = patchType === PATCH_TYPE.ALL - ? [PATCH_TYPE.MY, PATCH_TYPE.LIBRARY] - : R.of(patchType); - + renderPatches() { return ( // "calc(100% - 30px)" cause patch filtering buttons are 30px height
- {rendererKeys.map(k => this.patchRenderers[k]())} + {this.renderLocalPatches()} + {this.renderLibraryPatches()}
); @@ -338,17 +293,17 @@ class ProjectBrowser extends React.Component { /> + {this.renderPatches()} + { this.patchContextMenuRef = c; }} + onPatchAdd={this.onAddNode} + onPatchOpen={this.props.actions.switchPatch} + onPatchDelete={this.props.actions.requestDelete} + onPatchRename={this.props.actions.requestRename} + onPatchHelp={this.onPatchHelpClicked} /> - - {this.renderPatches} - ); } @@ -380,6 +335,8 @@ ProjectBrowser.propTypes = { startDraggingPatch: PropTypes.func.isRequired, renameProject: PropTypes.func.isRequired, closeAllPopups: PropTypes.func.isRequired, + showLibSuggester: PropTypes.func.isRequired, + showHelpbar: PropTypes.func.isRequired, }), }; @@ -416,6 +373,8 @@ const mapDispatchToProps = dispatch => ({ closeAllPopups: PopupActions.hideAllPopups, addNotification: MessagesActions.addNotification, + showLibSuggester: EditorActions.showLibSuggester, + showHelpbar: EditorActions.showHelpbar, }, dispatch), }); diff --git a/packages/xod-client/src/utils/components/Workarea.jsx b/packages/xod-client/src/utils/components/Workarea.jsx deleted file mode 100644 index 61d9900d..00000000 --- a/packages/xod-client/src/utils/components/Workarea.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const Workarea = ({ children }) => ( -
- {children} -
-); - -Workarea.propTypes = { - children: PropTypes.arrayOf(PropTypes.element), -}; - -export default Workarea; diff --git a/packages/xod-client/stories/PatchTypeSelector.jsx b/packages/xod-client/stories/PatchTypeSelector.jsx deleted file mode 100644 index 08164543..00000000 --- a/packages/xod-client/stories/PatchTypeSelector.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; - -import '../src/core/styles/main.scss'; -import PatchTypeSelector from '../src/projectBrowser/components/PatchTypeSelector'; - -const selectedOptionsRenderer = selectedKey => `selected ${selectedKey}`; - -storiesOf('PatchTypeSelector', module) - .addDecorator(story => ( -
-

inside a 300px wide div:

- {story()} -
- )) - .add('default', () => ( - - {selectedOptionsRenderer} - - )) - .add('with initial selection', () => ( - - {selectedOptionsRenderer} - - )) - .add('with no options', () => ( - - {selectedOptionsRenderer} - - )); - diff --git a/yarn.lock b/yarn.lock index c1f8d8d4..ee30ef2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8842,6 +8842,13 @@ react-collapsible@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/react-collapsible/-/react-collapsible-2.0.3.tgz#b7228b002428a6e0a9f41646ea61ba28aacfcb00" +react-contextmenu@^2.9.1: + version "2.9.1" + resolved "https://registry.yarnpkg.com/react-contextmenu/-/react-contextmenu-2.9.1.tgz#391c7001f73e49772e37e98d154736d50dac11de" + dependencies: + classnames "^2.2.5" + object-assign "^4.1.0" + react-custom-scroll@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/react-custom-scroll/-/react-custom-scroll-2.0.1.tgz#4775761a48d66c4b1e576c0705fc6a59fc0d89b6"