mirror of
https://github.com/xodio/xod.git
synced 2026-03-14 20:56:52 +01:00
feat(xod-client): implement contextmenu in the Project Browser, add Add Library button to ProjectBrowser PanelToolbar, update tests to work properly with new contextmenu & replace title attributes in PatchGroupItems with data-id attributes
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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 }
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
26
packages/xod-client/src/core/assets/icons/addlib.svg
Normal file
26
packages/xod-client/src/core/assets/icons/addlib.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="65%" x2="0%" y2="0%" id="grad">
|
||||
<stop stop-color="#CCCCCC" offset="0%"></stop>
|
||||
<stop stop-color="#CCCCCC" stop-opacity="0.3" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="none" fill="#CCCCCC" fill-rule="evenodd">
|
||||
<g transform="translate(7.234928, 3.804138)">
|
||||
<path d="M3.22194653,8.5042815 L3.37501987,9.075559 C3.43468126,9.29821833 3.66196692,9.43085249 3.88265771,9.37171857 L6.11372131,8.77390688 C6.33438445,8.71478037 6.46493017,8.48626558 6.40526878,8.26360626 L6.25219544,7.69232875 C5.30564341,7.94595661 4.51643548,8.15742423 3.22194653,8.5042815 Z"></path>
|
||||
<path d="M4.77608366,2.03378173 L6.05751679,6.8161553 L3.02729552,7.62810064 L0.993183359,0.0366906991 L2.70318474,0.0366906991 L2.70318474,2.03378173 L4.77608366,2.03378173 Z" fill="url(#grad)"></path>
|
||||
</g>
|
||||
<g transform="translate(4.091679, 1.000000)">
|
||||
<path d="M3.1621524,1.02243328 L3.1621524,0.436378977 C3.1621524,0.207931678 2.97691094,0.0226902189 2.74846364,0.0226902189 L0.438696641,0.0226902189 C0.210249343,0.0226902189 0.0250078832,0.207931678 0.0250078832,0.436378977 L0.0250078832,1.02243328 L3.1621524,1.02243328 Z"></path>
|
||||
<path d="M0.0250364963,11 L0.0250364963,11.5860543 C0.0250364963,11.814473 0.210249343,11.9997145 0.438725254,11.9997145 L2.74849226,11.9997145 C2.97693955,11.9997145 3.16218101,11.814473 3.16218101,11.5860543 L3.16218101,11 C2.18223824,11 1.36519007,11 0.0250364963,11 Z"></path>
|
||||
<polygon points="0.0250364963 1.5 3.1621524 1.5 3.1621524 10.5 0.0250364963 10.5"></polygon>
|
||||
</g>
|
||||
<g transform="translate(0.000000, 1.000000)">
|
||||
<path d="M3.15142247,1.02243328 L3.15142247,0.436378977 C3.15142247,0.207931678 2.96620963,0.0226902189 2.73773372,0.0226902189 L0.427995327,0.0226902189 C0.199519416,0.0226902189 0.0143065693,0.207931678 0.0143065693,0.436378977 L0.0143065693,1.02243328 L3.15142247,1.02243328 Z"></path>
|
||||
<path d="M0.0143065693,11 L0.0143065693,11.5860543 C0.0143065693,11.814473 0.199519416,11.9997145 0.427995327,11.9997145 L2.73773372,11.9997145 C2.96620963,11.9997145 3.15142247,11.814473 3.15142247,11.5860543 L3.15142247,11 C2.17153693,11 1.35448876,11 0.0143065693,11 Z"></path>
|
||||
<polygon points="0.0143065693 1.5 3.15142247 1.5 3.15142247 10.5 0.0143065693 10.5"></polygon>
|
||||
</g>
|
||||
<polygon fill-rule="nonzero" points="10.9985241 5 10.9985241 3.01317902 9 3.01317902 9 1.98682098 10.9985241 1.98682098 10.9985241 0 11.9975593 0 11.9975593 1.98682098 14 1.98682098 14 3.01317902 11.9975593 3.01317902 11.9975593 5"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="12px" height="13px" viewBox="0 0 12 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" fill-rule="nonzero" fill="#CCCCCC">
|
||||
<path d="M12,0.5 L0,0.5 L0,2 L12,2 L12,0.5 Z M12,4.5 L0,4.5 L0,6 L12,6 L12,4.5 Z M6.63512939e-16,10 L5,10 L5,8.5 L0,8.5 L6.63512939e-16,10 Z M6,8.5 L9.00078452,13 L12,8.5 L6,8.5 Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
20
packages/xod-client/src/core/assets/icons/libs.svg
Normal file
20
packages/xod-client/src/core/assets/icons/libs.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="14px" height="13px" viewBox="0 0 14 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(11.234928, 6.804138) rotate(-15.000000) translate(-11.234928, -6.804138) translate(9.234928, 0.804138)" fill="#6965BD">
|
||||
<path d="M3.1621524,1.02243328 L3.1621524,0.436378977 C3.1621524,0.207931678 2.97691094,0.0226902189 2.74846364,0.0226902189 L0.438696641,0.0226902189 C0.210249343,0.0226902189 0.0250078832,0.207931678 0.0250078832,0.436378977 L0.0250078832,1.02243328 L3.1621524,1.02243328 Z"></path>
|
||||
<path d="M0.0250364963,11 L0.0250364963,11.5860543 C0.0250364963,11.814473 0.210249343,11.9997145 0.438725254,11.9997145 L2.74849226,11.9997145 C2.97693955,11.9997145 3.16218101,11.814473 3.16218101,11.5860543 L3.16218101,11 C2.18223824,11 1.36519007,11 0.0250364963,11 Z"></path>
|
||||
<polygon points="0.0250364963 1.5 3.1621524 1.5 3.1621524 10.5 0.0250364963 10.5"></polygon>
|
||||
</g>
|
||||
<g transform="translate(4.091679, 1.000000)" fill="#2E8F6C">
|
||||
<path d="M3.1621524,1.02243328 L3.1621524,0.436378977 C3.1621524,0.207931678 2.97691094,0.0226902189 2.74846364,0.0226902189 L0.438696641,0.0226902189 C0.210249343,0.0226902189 0.0250078832,0.207931678 0.0250078832,0.436378977 L0.0250078832,1.02243328 L3.1621524,1.02243328 Z"></path>
|
||||
<path d="M0.0250364963,11 L0.0250364963,11.5860543 C0.0250364963,11.814473 0.210249343,11.9997145 0.438725254,11.9997145 L2.74849226,11.9997145 C2.97693955,11.9997145 3.16218101,11.814473 3.16218101,11.5860543 L3.16218101,11 C2.18223824,11 1.36519007,11 0.0250364963,11 Z"></path>
|
||||
<polygon points="0.0250364963 1.5 3.1621524 1.5 3.1621524 10.5 0.0250364963 10.5"></polygon>
|
||||
</g>
|
||||
<g transform="translate(0.000000, 1.000000)" fill="#B99039">
|
||||
<path d="M3.15142247,1.02243328 L3.15142247,0.436378977 C3.15142247,0.207931678 2.96620963,0.0226902189 2.73773372,0.0226902189 L0.427995327,0.0226902189 C0.199519416,0.0226902189 0.0143065693,0.207931678 0.0143065693,0.436378977 L0.0143065693,1.02243328 L3.15142247,1.02243328 Z"></path>
|
||||
<path d="M0.0143065693,11 L0.0143065693,11.5860543 C0.0143065693,11.814473 0.199519416,11.9997145 0.427995327,11.9997145 L2.73773372,11.9997145 C2.96620963,11.9997145 3.15142247,11.814473 3.15142247,11.5860543 L3.15142247,11 C2.17153693,11 1.35448876,11 0.0143065693,11 Z"></path>
|
||||
<polygon points="0.0143065693 1.5 3.15142247 1.5 3.15142247 10.5 0.0143065693 10.5"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
6
packages/xod-client/src/core/assets/icons/newpatch.svg
Normal file
6
packages/xod-client/src/core/assets/icons/newpatch.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="9px" height="12px" viewBox="0 0 9 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" fill-rule="nonzero" fill="#CCCCCC">
|
||||
<path d="M4,4 L4,0 L0,0 L0,12 L8,12 L8.00999451,4 L4,4 Z M5.01281203,0.00772932331 L5.01281203,3.00772932 L8.01281203,3.00772932 L5.01281203,0.00772932331 Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
16
packages/xod-client/src/core/assets/icons/project.svg
Normal file
16
packages/xod-client/src/core/assets/icons/project.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="11px" height="14px" viewBox="0 0 11 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" fill="#86CC23" fill-rule="evenodd">
|
||||
<g transform="translate(0.000000, 1.909091)">
|
||||
<path d="M5.432,0.159090909 C6.56727273,0.159090909 7.48681818,1.07927273 7.48681818,2.21454545 C7.48681818,3.34918182 6.56727273,4.26936364 5.432,4.26936364 C4.29672727,4.26936364 3.37654545,3.34918182 3.37654545,2.21454545 C3.37654545,1.07927273 4.29672727,0.159090909 5.432,0.159090909 Z" id="Shape"></path>
|
||||
<path d="M10.7112727,11.8853636 L10.7112727,10.4363636 L7.62745455,3.24290909 C7.62745455,3.24290909 7.34745455,4.53472727 5.712,4.83063636 C7.46390909,7.56381818 10.7112727,11.8853636 10.7112727,11.8853636 Z" id="Shape"></path>
|
||||
<path d="M0.106272727,11.9318182 L0.106272727,10.4840909 L3.18945455,3.28936364 C3.18945455,3.28936364 3.46945455,4.58181818 5.10490909,4.87772727 C3.353,7.61027273 0.106272727,11.9318182 0.106272727,11.9318182 Z" id="Shape"></path>
|
||||
</g>
|
||||
<g transform="translate(0.909091, 8.545455)" fill-rule="nonzero">
|
||||
<path d="M7.87822867,0.923438142 C7.50410323,1.18949595 7.09820278,1.40829478 6.66962926,1.57415679 C6.03362099,1.82033432 5.35590536,1.94836364 4.661,1.94836364 C4.01340655,1.94836364 3.38143404,1.8376875 2.78591106,1.62394047 C2.31041663,1.45362267 1.86254208,1.22021084 1.45394051,0.930613746 L0.875695852,1.74647716 C1.35850858,2.08867155 1.88746937,2.36434183 2.44839443,2.56525998 C3.15165621,2.81767708 3.89789035,2.94836364 4.661,2.94836364 C5.48006451,2.94836364 6.28010443,2.79722566 7.0305757,2.50674334 C7.53681808,2.31082274 8.01612837,2.05245266 8.45777133,1.73838004 L7.87822867,0.923438142 Z" id="Shape"></path>
|
||||
</g>
|
||||
<g transform="translate(4.454545, 0.000000)">
|
||||
<rect x="0" y="0" width="1.90909091" height="1.27272727"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -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;
|
||||
|
||||
|
||||
21
packages/xod-client/src/core/styles/abstracts/icons.scss
Normal file
21
packages/xod-client/src/core/styles/abstracts/icons.scss
Normal file
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
flex-wrap: nowrap;
|
||||
|
||||
background: $sidebar-color-bg;
|
||||
border-right: 2px solid $chrome-outlines;
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
.Workarea-container {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Workarea {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
>
|
||||
<FocusTrap
|
||||
className="ProjectBrowser-container"
|
||||
onFocus={() => this.props.actions.setFocusedArea(FOCUS_AREAS.PROJECT_BROWSER)}
|
||||
onFocus={this.onProjectBrowserFocus}
|
||||
>
|
||||
<ProjectBrowser />
|
||||
</FocusTrap>
|
||||
<FocusTrap
|
||||
className="Inspector-container"
|
||||
onFocus={() => this.props.actions.setFocusedArea(FOCUS_AREAS.INSPECTOR)}
|
||||
onFocus={this.onInspectorFocus}
|
||||
>
|
||||
<Inspector
|
||||
selection={selection}
|
||||
@@ -223,21 +245,19 @@ class Editor extends React.Component {
|
||||
/>
|
||||
</FocusTrap>
|
||||
</Sidebar>
|
||||
{suggester}
|
||||
{libSuggester}
|
||||
<FocusTrap
|
||||
className="Workarea-container"
|
||||
onFocus={() => this.props.actions.setFocusedArea(FOCUS_AREAS.WORKAREA)}
|
||||
className="Workarea"
|
||||
onFocus={this.onWorkareaFocus}
|
||||
>
|
||||
<Workarea>
|
||||
<Tabs />
|
||||
{DebugSessionStopButton}
|
||||
{this.renderOpenedPatchTab()}
|
||||
{suggester}
|
||||
{libSuggester}
|
||||
{BreadcrumbsContainer}
|
||||
{this.renderOpenedImplementationEditorTabs()}
|
||||
{DebuggerContainer}
|
||||
<SnackBar />
|
||||
</Workarea>
|
||||
<Tabs />
|
||||
{DebugSessionStopButton}
|
||||
{this.renderOpenedPatchTab()}
|
||||
{BreadcrumbsContainer}
|
||||
{this.renderOpenedImplementationEditorTabs()}
|
||||
{DebuggerContainer}
|
||||
<SnackBar />
|
||||
</FocusTrap>
|
||||
<Helpbar />
|
||||
<AccountPane />
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 }) => (
|
||||
<Collapsible
|
||||
classParentString="PatchGroup"
|
||||
trigger={<span className="patch-group-trigger" title={name}>{name}</span>}
|
||||
trigger={
|
||||
<span className="patch-group-trigger" data-id={name}>{name}</span>
|
||||
}
|
||||
triggerClassName={type}
|
||||
triggerOpenedClassName={type}
|
||||
transitionTime={100}
|
||||
|
||||
@@ -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(
|
||||
<div
|
||||
title={label}
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
role="button"
|
||||
data-id={label}
|
||||
className={classNames}
|
||||
onClick={onClick}
|
||||
onContextMenuCapture={onClick}
|
||||
{...R.omit(['patchPath', 'onBeginDrag', 'connectDragPreview', 'dead'], restProps)}
|
||||
>
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className="PatchGroupItem__label"
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
role="button"
|
||||
<ContextMenuTrigger
|
||||
id={PATCH_GROUP_CONTEXT_MENU_ID}
|
||||
holdToDisplay={-1}
|
||||
collect={collectPropsFn}
|
||||
>
|
||||
{(dead) ? deadIcon : null}
|
||||
{label}
|
||||
</div>
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className="PatchGroupItem__label"
|
||||
onDoubleClick={onDoubleClick}
|
||||
role="button"
|
||||
>
|
||||
{(dead) ? deadIcon : null}
|
||||
{label}
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<div className="PatchGroupItem__hover-buttons">
|
||||
{hoverButtons}
|
||||
{/* placeholder for context menu opener */}
|
||||
{/* <i className="fa fa-bars" /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
? (
|
||||
<MenuItem
|
||||
onClick={callCallbackWithPatchPath(props.onPatchRename)}
|
||||
// data-id — special attribute that used by func tests
|
||||
attributes={{ 'data-id': 'rename' }}
|
||||
>
|
||||
Rename
|
||||
</MenuItem>
|
||||
) : null;
|
||||
|
||||
const deletePatch = (trigger.isLocalPatch)
|
||||
? (
|
||||
<MenuItem
|
||||
onClick={callCallbackWithPatchPath(props.onPatchDelete)}
|
||||
attributes={{ 'data-id': 'delete' }}
|
||||
>
|
||||
Delete
|
||||
</MenuItem>
|
||||
) : 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 (
|
||||
<ContextMenu
|
||||
id={PATCH_GROUP_CONTEXT_MENU_ID}
|
||||
className={cls}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={callCallbackWithPatchPath(props.onPatchAdd)}
|
||||
disabled={!trigger.canAdd}
|
||||
attributes={{ 'data-id': 'place' }}
|
||||
>
|
||||
<span className="accelerator">
|
||||
drag&drop
|
||||
</span>
|
||||
Place
|
||||
</MenuItem>
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={callCallbackWithPatchPath(props.onPatchOpen)}
|
||||
attributes={{ 'data-id': 'open' }}
|
||||
>
|
||||
<span className="accelerator">
|
||||
click×2
|
||||
</span>
|
||||
Open
|
||||
</MenuItem>
|
||||
{renamePatch}
|
||||
{deletePatch}
|
||||
<MenuItem divider />
|
||||
<MenuItem
|
||||
onClick={callCallbackWithPatchPath(props.onPatchHelp)}
|
||||
attributes={{ 'data-id': 'help' }}
|
||||
>
|
||||
<span className="accelerator">
|
||||
h
|
||||
</span>
|
||||
Help
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -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 (
|
||||
<div className="PatchTypeSelector">
|
||||
<ul className="options">
|
||||
{options.map(({ key, name }) =>
|
||||
<li
|
||||
key={key}
|
||||
className={cn('option', { selected: key === selectedOptionKey })}
|
||||
onClick={() => this.onSelect(key)}
|
||||
>
|
||||
{name}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{children(selectedOptionKey)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -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 }) => (
|
||||
<div className="ProjectBrowserToolbar">
|
||||
<div className="ProjectBrowserToolbar-left">
|
||||
<div className="ProjectBrowserToolbar-title">
|
||||
Project Browser
|
||||
</div>
|
||||
<div className="ProjectBrowserToolbar-buttons">
|
||||
<button
|
||||
className="newpatch"
|
||||
title="Add patch"
|
||||
onClick={onClickAddPatch}
|
||||
>
|
||||
<Icon name="file" />
|
||||
</button>
|
||||
/>
|
||||
<button
|
||||
className="addlib"
|
||||
title="Add library"
|
||||
onClick={onClickAddLibrary}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -19,6 +25,7 @@ ProjectBrowserToolbar.displayName = 'ProjectBrowserToolbar';
|
||||
|
||||
ProjectBrowserToolbar.propTypes = {
|
||||
onClickAddPatch: PropTypes.func.isRequired,
|
||||
onClickAddLibrary: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ProjectBrowserToolbar;
|
||||
|
||||
3
packages/xod-client/src/projectBrowser/constants.js
Normal file
3
packages/xod-client/src/projectBrowser/constants.js
Normal file
@@ -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';
|
||||
@@ -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 [
|
||||
<Icon
|
||||
key="delete"
|
||||
name="trash"
|
||||
title="Delete patch"
|
||||
className="hover-button"
|
||||
onClick={() => requestDelete(patchPath)}
|
||||
/>,
|
||||
<Icon
|
||||
key="rename"
|
||||
name="pencil"
|
||||
title="Rename patch"
|
||||
className="hover-button"
|
||||
onClick={() => 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 (
|
||||
<Icon
|
||||
key="add"
|
||||
name="plus-circle"
|
||||
title="Add node"
|
||||
className={classNames}
|
||||
onClick={action}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDocsButton(patchPath) { // eslint-disable-line class-methods-use-this
|
||||
return (
|
||||
<a
|
||||
href={getUtmSiteUrl(`/libs/${patchPath}`, 'docs', 'project-browser')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover-button"
|
||||
key="patch-guide-button"
|
||||
title="Open documentation in web browser"
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
renderHoveredButtons(collectPropsFn) {
|
||||
return [
|
||||
<ContextMenuTrigger
|
||||
key="contextMenuTrigger"
|
||||
id={PATCH_GROUP_CONTEXT_MENU_ID}
|
||||
renderTag="div"
|
||||
attributes={{
|
||||
className: 'contextmenu',
|
||||
}}
|
||||
collect={collectPropsFn}
|
||||
holdToDisplay={0}
|
||||
>
|
||||
<IconGuide
|
||||
className="project-browser--guide-button"
|
||||
width="14px"
|
||||
height="14px"
|
||||
fill="#FFF"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
{/* Component needs at least one child :-( */}
|
||||
<span />
|
||||
</ContextMenuTrigger>,
|
||||
];
|
||||
}
|
||||
|
||||
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 (
|
||||
<PatchGroupItem
|
||||
key={path}
|
||||
patchPath={path}
|
||||
@@ -232,9 +194,17 @@ class ProjectBrowser extends React.Component {
|
||||
onBeginDrag={startDraggingPatch}
|
||||
isSelected={path === selectedPatchPath}
|
||||
onClick={() => setSelection(path)}
|
||||
hoverButtons={this.localPatchesHoveredButtons(path)}
|
||||
collectPropsFn={collectPropsFn}
|
||||
hoverButtons={this.renderHoveredButtons(collectPropsFn)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderLocalPatches() {
|
||||
const {
|
||||
projectName,
|
||||
localPatches,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PatchGroup
|
||||
@@ -243,18 +213,18 @@ class ProjectBrowser extends React.Component {
|
||||
name={projectName}
|
||||
onClose={this.deselectIfInLocalPatches}
|
||||
>
|
||||
{R.map(renderItem, localPatches)}
|
||||
{localPatches.map(this.renderItem)}
|
||||
</PatchGroup>
|
||||
);
|
||||
}
|
||||
|
||||
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: (<div
|
||||
key={`${owner}/${name}/${version}`}
|
||||
className="PatchGroup PatchGroup--installing library"
|
||||
>
|
||||
<span className="name">{owner}/{name}</span>
|
||||
@@ -279,19 +249,7 @@ class ProjectBrowser extends React.Component {
|
||||
name={libName}
|
||||
onClose={this.deselectIfInLibrary(libName)}
|
||||
>
|
||||
{libPatches.map(({ path, dead }) =>
|
||||
<PatchGroupItem
|
||||
key={path}
|
||||
patchPath={path}
|
||||
dead={dead}
|
||||
label={getBaseName(path)}
|
||||
isSelected={path === selectedPatchPath}
|
||||
onClick={() => setSelection(path)}
|
||||
onDoubleClick={() => switchPatch(path)}
|
||||
onBeginDrag={startDraggingPatch}
|
||||
hoverButtons={this.libraryPatchesHoveredButtons(path)}
|
||||
/>
|
||||
)}
|
||||
{libPatches.map(this.renderItem)}
|
||||
</PatchGroup>),
|
||||
})),
|
||||
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
|
||||
<CustomScroll heightRelativeToParent="calc(100% - 30px)">
|
||||
<div className="patches-list">
|
||||
{rendererKeys.map(k => this.patchRenderers[k]())}
|
||||
{this.renderLocalPatches()}
|
||||
{this.renderLibraryPatches()}
|
||||
</div>
|
||||
</CustomScroll>
|
||||
);
|
||||
@@ -338,17 +293,17 @@ class ProjectBrowser extends React.Component {
|
||||
/>
|
||||
<ProjectBrowserToolbar
|
||||
onClickAddPatch={this.props.actions.requestCreatePatch}
|
||||
onClickAddLibrary={this.onClickAddLibrary}
|
||||
/>
|
||||
{this.renderPatches()}
|
||||
<PatchGroupItemContextMenu
|
||||
ref={(c) => { this.patchContextMenuRef = c; }}
|
||||
onPatchAdd={this.onAddNode}
|
||||
onPatchOpen={this.props.actions.switchPatch}
|
||||
onPatchDelete={this.props.actions.requestDelete}
|
||||
onPatchRename={this.props.actions.requestRename}
|
||||
onPatchHelp={this.onPatchHelpClicked}
|
||||
/>
|
||||
<PatchTypeSelector
|
||||
options={[
|
||||
{ key: PATCH_TYPE.ALL, name: 'All' },
|
||||
{ key: PATCH_TYPE.LIBRARY, name: 'Libraries' },
|
||||
{ key: PATCH_TYPE.MY, name: 'My Patches' },
|
||||
]}
|
||||
onChange={this.props.actions.removeSelection}
|
||||
>
|
||||
{this.renderPatches}
|
||||
</PatchTypeSelector>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Workarea = ({ children }) => (
|
||||
<div className="Workarea">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Workarea.propTypes = {
|
||||
children: PropTypes.arrayOf(PropTypes.element),
|
||||
};
|
||||
|
||||
export default Workarea;
|
||||
@@ -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 => (
|
||||
<div style={{ width: 300 }}>
|
||||
<p>inside a 300px wide div:</p>
|
||||
{story()}
|
||||
</div>
|
||||
))
|
||||
.add('default', () => (
|
||||
<PatchTypeSelector
|
||||
options={[
|
||||
{ key: 'foo', name: 'Foo' },
|
||||
{ key: 'bar', name: 'Bar' },
|
||||
]}
|
||||
>
|
||||
{selectedOptionsRenderer}
|
||||
</PatchTypeSelector>
|
||||
))
|
||||
.add('with initial selection', () => (
|
||||
<PatchTypeSelector
|
||||
options={[
|
||||
{ key: 'foo', name: 'Foo' },
|
||||
{ key: 'bar', name: 'Bar' },
|
||||
{ key: 'baz', name: 'Baz' },
|
||||
]}
|
||||
initialSelectedKey="bar"
|
||||
>
|
||||
{selectedOptionsRenderer}
|
||||
</PatchTypeSelector>
|
||||
))
|
||||
.add('with no options', () => (
|
||||
<PatchTypeSelector
|
||||
options={[]}
|
||||
>
|
||||
{selectedOptionsRenderer}
|
||||
</PatchTypeSelector>
|
||||
));
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user