From d5a47c84c56cfa72a32071b4c85acb7c5361f566 Mon Sep 17 00:00:00 2001 From: Oskar Damkjaer Date: Mon, 22 Feb 2021 10:48:32 +0100 Subject: [PATCH] Favorites ux refresh (#1278) * Refactor button components * Improve types by removing 'any' * Remove unused typedoc and rename files * Fix drag and drop typing * Basic typing of folders duck * Update types for folder/favorites duck * renaming works again * Improve redux typings further * Update folder structures * Select working properly * Allows renaming and removal of folders * Remove folders touchup * Add drag and drop * Fix tests and bug with useEffect * Restore dev config * Fix project files * Fix project files * Avoid pre-loading all saved scripts * Move files * Further fixing * Fix favorites duck test * Removed need for fav.utils.test * Fix saved scripts utils test * Fix last unit tests * Allow adding empty favorites * Add export Favorite saved automatically Function to export as cypherfile Fix issue with font lig Implement multiselection Nicer context menu stay selected Drag multiple Fix naming bug Improve static scripts Cleanup Remove unused imports Better drag Improve pattern check Only missing icons Rebase issues Align icons Fix text in dark mode * Update unit tests * Fix up tests and add bulk delete test * Make rename clickable in context menu * Fix review comments * Solve bug where icons were invisible * Use only shift for selection * Fix icon alignment * Add three dots to folders as well * Don't open dialog for naming favorite * Easy naming of folder * UpdateTests * UI touch ups * Fix folder * Work on e2e tests * update react drag-n-drop * Solve bug with drag and drop * Forgot to remove it.only --- LICENSES.txt | 181 ++++++------ NOTICE.txt | 31 +-- e2e_tests/integration/0.index.spec.ts | 18 +- e2e_tests/integration/saved-scripts.spec.ts | 55 ++-- package.json | 5 +- .../ProjectFiles/ProjectFilesList.tsx | 38 +-- .../ProjectFiles/ProjectFilesListItem.tsx | 5 +- .../ProjectFiles/ProjectsFilesScripts.tsx | 52 +--- .../ProjectFiles/projectFilesConstants.ts | 30 -- .../ProjectFiles/projectFilesUtils.ts | 87 +----- .../components/SavedScripts/SavedScripts.tsx | 257 +++++++++++++----- .../SavedScripts/SavedScriptsButton.tsx | 54 +++- .../SavedScripts/SavedScriptsFolder.tsx | 123 ++++++--- .../SavedScripts/SavedScriptsListItem.tsx | 148 +++++++--- src/browser/components/SavedScripts/styled.ts | 114 ++++++-- src/browser/components/icons/Icons.tsx | 13 + src/browser/icons/add-circle.svg | 11 + src/browser/icons/app-window-code.svg | 1 - src/browser/icons/folder-add.svg | 20 ++ src/browser/icons/folder-empty.svg | 11 + src/browser/icons/hollow-run-icon.svg | 5 + .../icons/navigation-menu-vertical.svg | 15 + src/browser/init.ts | 1 + src/browser/modules/App/App.tsx | 7 +- src/browser/modules/Editor/EditorFrame.tsx | 13 +- src/browser/modules/Frame/FrameTitlebar.tsx | 5 +- .../modules/Sidebar/NewSavedScript.tsx | 8 +- src/browser/modules/Sidebar/ProjectFiles.tsx | 38 +-- src/browser/modules/Sidebar/Sidebar.tsx | 60 +--- src/browser/modules/Sidebar/favorites.ts | 26 +- src/browser/modules/Sidebar/styled.tsx | 2 +- .../modules/Stream/SysInfoFrame/index.tsx | 3 +- src/browser/styles/themes.ts | 1 + src/browser/styles/util-classes.css | 4 + src/shared/modules/favorites/favoritesDuck.ts | 8 +- .../modules/sidebar/sidebarDuck.test.ts | 12 +- src/shared/modules/sidebar/sidebarDuck.ts | 38 ++- .../services/exporting/favoriteUtils.ts | 12 + yarn.lock | 93 +++---- 39 files changed, 910 insertions(+), 695 deletions(-) create mode 100644 src/browser/icons/add-circle.svg create mode 100644 src/browser/icons/folder-add.svg create mode 100644 src/browser/icons/folder-empty.svg create mode 100644 src/browser/icons/hollow-run-icon.svg create mode 100644 src/browser/icons/navigation-menu-vertical.svg create mode 100644 src/browser/styles/util-classes.css diff --git a/LICENSES.txt b/LICENSES.txt index ba348d08656..3dae7151dc4 100644 --- a/LICENSES.txt +++ b/LICENSES.txt @@ -836,6 +836,82 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ----- +The following software may be included in this product: @react-dnd/asap. A copy of the source code may be downloaded from https://github.com/kriskowal/asap.git. This software contains the following license and notice below: + +Copyright 2009–2014 Contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +----- + +The following software may be included in this product: @react-dnd/invariant, invariant, prop-types, warning. A copy of the source code may be downloaded from https://github.com/react-dnd/invariant (@react-dnd/invariant), https://github.com/zertosh/invariant (invariant), https://github.com/facebook/prop-types.git (prop-types), https://github.com/BerkeleyTrue/warning.git (warning). This software contains the following license and notice below: + +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----- + +The following software may be included in this product: @react-dnd/shallowequal, shallowequal. A copy of the source code may be downloaded from https://github.com/react-dnd/shallowequal.git (@react-dnd/shallowequal), https://github.com/dashed/shallowequal.git (shallowequal). This software contains the following license and notice below: + +MIT License + +Copyright (c) 2017 Alberto Leal (github.com/dashed) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----- + The following software may be included in this product: @relate-by-ui/css, @relate-by-ui/relatable. A copy of the source code may be downloaded from https://github.com/neo4j-apps/relate-by-ui (@relate-by-ui/css), https://github.com/neo4j-apps/relatable (@relate-by-ui/relatable). This software contains the following license and notice below: Apache License @@ -1180,7 +1256,7 @@ SOFTWARE. ----- -The following software may be included in this product: @types/asap, @types/color-name, @types/hoist-non-react-statics, @types/long, @types/mdast, @types/parse-json, @types/prop-types, @types/shallowequal, @types/unist. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/asap), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/color-name), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/long), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mdast), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/shallowequal), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/unist). This software contains the following license and notice below: +The following software may be included in this product: @types/color-name, @types/hoist-non-react-statics, @types/long, @types/mdast, @types/parse-json, @types/prop-types, @types/unist. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/color-name), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hoist-non-react-statics), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/long), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/mdast), https://www.github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse-json), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/prop-types), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/unist). This software contains the following license and notice below: MIT License @@ -1206,7 +1282,7 @@ MIT License ----- -The following software may be included in this product: @types/hast, @types/invariant, @types/node, @types/parse5, @types/react, @types/zen-observable. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hast), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/invariant), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse5), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: +The following software may be included in this product: @types/hast, @types/node, @types/parse5, @types/react, @types/zen-observable. A copy of the source code may be downloaded from https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/hast), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/node), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/parse5), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/react), https://github.com/DefinitelyTyped/DefinitelyTyped.git (@types/zen-observable). This software contains the following license and notice below: MIT License @@ -1464,30 +1540,6 @@ THE SOFTWARE. ----- -The following software may be included in this product: asap. A copy of the source code may be downloaded from https://github.com/kriskowal/asap.git. This software contains the following license and notice below: - -Copyright 2009–2014 Contributors. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. - ------ - The following software may be included in this product: ascii-data-table. A copy of the source code may be downloaded from https://github.com/oskarhane/ascii-data-table.git. This software contains the following license and notice below: The MIT License (MIT) @@ -3171,31 +3223,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ----- -The following software may be included in this product: dateformat. A copy of the source code may be downloaded from https://github.com/felixge/node-dateformat.git. This software contains the following license and notice below: - -(c) 2007-2009 Steven Levithan - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ------ - The following software may be included in this product: debug. A copy of the source code may be downloaded from git://github.com/visionmedia/debug.git. This software contains the following license and notice below: (The MIT License) @@ -4755,32 +4782,6 @@ PERFORMANCE OF THIS SOFTWARE. ----- -The following software may be included in this product: invariant, prop-types, warning. A copy of the source code may be downloaded from https://github.com/zertosh/invariant (invariant), https://github.com/facebook/prop-types.git (prop-types), https://github.com/BerkeleyTrue/warning.git (warning). This software contains the following license and notice below: - -MIT License - -Copyright (c) 2013-present, Facebook, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - The following software may be included in this product: is-arguments, is-negative-zero, is-regex, object-is. A copy of the source code may be downloaded from git://github.com/ljharb/is-arguments.git (is-arguments), git://github.com/ljharb/is-negative-zero.git (is-negative-zero), git://github.com/ljharb/is-regex.git (is-regex), git://github.com/es-shims/object-is.git (object-is). This software contains the following license and notice below: The MIT License (MIT) @@ -8726,32 +8727,6 @@ License, as follows: ----- -The following software may be included in this product: shallowequal. A copy of the source code may be downloaded from https://github.com/dashed/shallowequal.git. This software contains the following license and notice below: - -MIT License - -Copyright (c) 2017 Alberto Leal (github.com/dashed) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ------ - The following software may be included in this product: source-map. A copy of the source code may be downloaded from http://github.com/mozilla/source-map.git. This software contains the following license and notice below: Copyright (c) 2009-2011, Mozilla Foundation and contributors diff --git a/NOTICE.txt b/NOTICE.txt index cba46311e4f..3f7d95060c7 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -642,6 +642,15 @@ Third-party licenses │ │ ├─ URL: https://github.com/mdx-js/mdx.git │ │ ├─ VendorName: John Otander │ │ └─ VendorUrl: https://mdxjs.com/ +│ ├─ @react-dnd/asap@4.0.0 +│ │ └─ URL: https://github.com/kriskowal/asap.git +│ ├─ @react-dnd/invariant@2.0.0 +│ │ ├─ URL: https://github.com/react-dnd/invariant +│ │ └─ VendorName: Andres Suarez +│ ├─ @react-dnd/shallowequal@2.0.0 +│ │ ├─ URL: https://github.com/react-dnd/shallowequal.git +│ │ ├─ VendorName: Alberto Leal +│ │ └─ VendorUrl: github.com/dashed │ ├─ @scarf/scarf@0.1.7 │ │ ├─ URL: git+https://github.com/scarf-sh/scarf-js.git │ │ ├─ VendorName: Scarf Systems @@ -661,16 +670,12 @@ Third-party licenses │ │ ├─ URL: https://github.com/stardust-ui/react.git │ │ ├─ VendorName: Oleksandr Fediashov │ │ └─ VendorUrl: https://github.com/stardust-ui/react/tree/master/packages/react-component-ref -│ ├─ @types/asap@2.0.0 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/color-name@1.1.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/hast@2.3.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/hoist-non-react-statics@3.3.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/invariant@2.2.34 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/long@4.0.1 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/mdast@3.0.3 @@ -685,9 +690,7 @@ Third-party licenses │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/prop-types@15.7.3 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/react@16.9.49 -│ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git -│ ├─ @types/shallowequal@1.1.1 +│ ├─ @types/react@17.0.2 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git │ ├─ @types/unist@2.0.3 │ │ └─ URL: https://github.com/DefinitelyTyped/DefinitelyTyped.git @@ -753,8 +756,6 @@ Third-party licenses │ │ ├─ URL: https://github.com/sindresorhus/arrify.git │ │ ├─ VendorName: Sindre Sorhus │ │ └─ VendorUrl: sindresorhus.com -│ ├─ asap@2.0.6 -│ │ └─ URL: https://github.com/kriskowal/asap.git │ ├─ ascii-data-table@2.1.1 │ │ ├─ URL: https://github.com/oskarhane/ascii-data-table.git │ │ ├─ VendorName: Oskar Hane @@ -913,17 +914,13 @@ Third-party licenses │ ├─ csstype@2.6.13 │ │ ├─ URL: https://github.com/frenic/csstype │ │ └─ VendorName: Fredrik Nicol -│ ├─ csstype@3.0.3 +│ ├─ csstype@3.0.6 │ │ ├─ URL: https://github.com/frenic/csstype │ │ └─ VendorName: Fredrik Nicol │ ├─ dashdash@1.14.1 │ │ ├─ URL: git://github.com/trentm/node-dashdash.git │ │ ├─ VendorName: Trent Mick │ │ └─ VendorUrl: http://trentm.com -│ ├─ dateformat@3.0.3 -│ │ ├─ URL: https://github.com/felixge/node-dateformat.git -│ │ ├─ VendorName: Steven Levithan -│ │ └─ VendorUrl: https://github.com/felixge/node-dateformat │ ├─ debug@4.1.1 │ │ ├─ URL: git://github.com/visionmedia/debug.git │ │ └─ VendorName: TJ Holowaychuk @@ -961,7 +958,7 @@ Third-party licenses │ │ ├─ URL: https://github.com/dtudury/discontinuous-range.git │ │ ├─ VendorName: David Tudury │ │ └─ VendorUrl: https://github.com/dtudury/discontinuous-range -│ ├─ dnd-core@9.5.1 +│ ├─ dnd-core@11.1.3 │ │ └─ URL: https://github.com/react-dnd/react-dnd.git │ ├─ ecc-jsbn@0.1.2 │ │ ├─ URL: https://github.com/quartzjer/ecc-jsbn.git @@ -1496,9 +1493,9 @@ Third-party licenses │ │ ├─ URL: git://github.com/fent/randexp.js.git │ │ ├─ VendorName: Roly Fentanes │ │ └─ VendorUrl: http://fent.github.io/randexp.js/ -│ ├─ react-dnd-html5-backend@9.3.2 +│ ├─ react-dnd-html5-backend@11.1.3 │ │ └─ URL: https://github.com/react-dnd/react-dnd.git -│ ├─ react-dnd@9.3.2 +│ ├─ react-dnd@11.1.3 │ │ └─ URL: https://github.com/react-dnd/react-dnd.git │ ├─ react-dom@16.13.1 │ │ ├─ URL: https://github.com/facebook/react.git diff --git a/e2e_tests/integration/0.index.spec.ts b/e2e_tests/integration/0.index.spec.ts index 8b04a39db53..d77e5496604 100644 --- a/e2e_tests/integration/0.index.spec.ts +++ b/e2e_tests/integration/0.index.spec.ts @@ -147,19 +147,14 @@ describe('Neo4j Browser', () => { // Browser sync is disabled on Aura if (!isAura()) { it('will clear local storage when clicking "Clear local data"', () => { - const scriptName = 'foo' - cy.get(Editor).type( - `//${scriptName} - RETURN 1{enter}`, - { force: true } - ) + cy.connect('neo4j', Cypress.config('password')) - cy.get('[data-testid="frame-Favorite"]').click() - cy.get('[data-testid="saveScript"]').click() + cy.get(Editor).type(`RETURN 1{enter}`, { force: true }) - cy.get('.saved-scripts-list-item') + cy.get('[data-testid="frame-Favorite"]').click() + cy.get('[data-testid="savedScriptListItem"]') .first() - .should('be', scriptName) + .contains('RETURN 1') cy.get('[data-testid="drawerSync"]').click() cy.get('[data-testid="clearLocalData"]').click() @@ -169,7 +164,8 @@ describe('Neo4j Browser', () => { cy.get('[data-testid="clearLocalData"]').click() cy.get('[data-testid="drawerFavorites"]').click() - cy.get('.saved-scripts-list-item').should('have.length', 0) + + cy.get('[data-testid="savedScriptListItem"]').should('have.length', 0) cy.get('[data-testid="drawerFavorites"]').click() // once data is cleared the user is logged out and the connect form is displayed diff --git a/e2e_tests/integration/saved-scripts.spec.ts b/e2e_tests/integration/saved-scripts.spec.ts index 78df0c01b9d..2251ea532a2 100644 --- a/e2e_tests/integration/saved-scripts.spec.ts +++ b/e2e_tests/integration/saved-scripts.spec.ts @@ -25,21 +25,19 @@ describe('Saved Scripts', () => { cy.visit(Cypress.config('url')) .title() .should('include', 'Neo4j Browser') - //cy.wait(3000) + cy.wait(3000) cy.connect('neo4j', Cypress.config('password')) }) it('can save a result as favorite', () => { cy.executeCommand('RETURN 1') cy.get('[data-testid=frame-Favorite]').click() - cy.get('[data-testid=scriptName]') - .clear() - .type('script name') - cy.get('[data-testid=saveScript]').click() // saved in the list and can populate editor - cy.get('[data-testid="scriptTitle-script name"]').click() - cy.get('[data-testid="currentlyEditing"]').contains('script name') + cy.get('[data-testid="navicon-RETURN 1"').click({ force: true }) + cy.get('[data-testid="contextMenuEdit"').click() + + cy.get('[data-testid="currentlyEditing"]').contains('RETURN 1') // Editing script updates name and content cy.get('[data-testid="activeEditor"] textarea') .type( @@ -54,26 +52,20 @@ describe('Saved Scripts', () => { cy.get('[data-testid="currentlyEditing"]').contains('Guide') cy.get('[data-testid=savedScriptsButton-Run]').click() cy.getFrames().contains('Movie Graph') + + // can delete + cy.get('[data-testid="navicon-Guide"').click({ force: true }) + cy.get('[data-testid="contextMenuDelete"').click() }) - it('it can drag and drop a favorite in a folder', () => { + it('it can drag and drop a favorite in a folder', () => { cy.get('[data-testid=editor-discard]').click() cy.executeCommand(':clear') cy.executeCommand(':help cypher') cy.get('[data-testid=frame-Favorite]').click() - cy.get('[data-testid=scriptName]') - .invoke('val') - .should('equal', ':help cypher') - - cy.get('[data-testid=saveScript]').click() cy.get('[data-testid="savedScriptsButton-New folder"]').click() - cy.get('[data-testid="savedScriptsButton-Edit"]') - .eq(2) - .click({ force: true }) - cy.get('[data-testid="editSavedScriptFolderName"]') - .clear() - .type('fldr{enter}') + cy.get('[data-testid=editSavedScriptFolderName]').type('fldr{enter}') cy.get('[data-testid="scriptTitle-:help cypher"]').trigger('dragstart') @@ -89,5 +81,30 @@ describe('Saved Scripts', () => { cy.get('[data-testid="scriptTitle-:help cypher"]').should('not.exist') cy.get('[data-testid=expandFolder-fldr]').click() cy.get('[data-testid="scriptTitle-:help cypher"]').should('exist') + + // cleanup and delete the folder as well + cy.get('[data-testid=navicon-fldr]').click({ force: true }) + cy.get('[data-testid=contextMenuRename]').click() + cy.get('[data-testid=expandFolder-fldr]').should('not.exist') + cy.get('[data-testid=drawerFavorites]').click() + }) + + it('it can use bulk delete', () => { + cy.get('[data-testid=drawerFavorites]').click() + cy.get('[data-testid=createNewFavorite]') + .click() + .click() + .click() + + const mod = Cypress.platform === 'darwin' ? '{cmd}' : '{ctrl}' + // workaround to get meta clicks + cy.get('body').type(mod, { force: true, release: false }) + + cy.get('[data-testid="scriptTitle-Untitled favorite"]') + .should('have.length', 3) + .click({ multiple: true }) + cy.get('body').type(mod) // release mod + cy.get('[data-testid=savedScriptsButton-Remove]').click() + cy.get('[data-testid="scriptTitle-Untitled favorite"]').should('not.exist') }) }) diff --git a/package.json b/package.json index e307326bbba..d43ad5327ab 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,6 @@ "core-js": "3", "cypher-editor-support": "^1.1.7", "d3": "3", - "dateformat": "^3.0.3", "deepmerge": "^2.1.1", "file-saver": "^1.3.8", "firebase": "^7.13.1", @@ -183,8 +182,8 @@ "neo4j-driver": "^4.2.2", "prop-types": "^15.7.2", "react": "^16.9.0", - "react-dnd": "9.3.2", - "react-dnd-html5-backend": "9.3.2", + "react-dnd": "^11.1.3", + "react-dnd-html5-backend": "^11.1.3", "react-dom": "^16.9.0", "react-redux": "^5.0.7", "react-suber": "1.0.4", diff --git a/src/browser/components/ProjectFiles/ProjectFilesList.tsx b/src/browser/components/ProjectFiles/ProjectFilesList.tsx index eaf7b4636f9..67550c5b824 100644 --- a/src/browser/components/ProjectFiles/ProjectFilesList.tsx +++ b/src/browser/components/ProjectFiles/ProjectFilesList.tsx @@ -18,12 +18,7 @@ * along with this program. If not, see . */ import React from 'react' -import { - SavedScriptsMain, - SavedScriptsBody, - SavedScriptsBodySection, - SavedScriptsHeader -} from '../SavedScripts/styled' +import { SavedScriptsBody, SavedScriptsHeader } from '../SavedScripts/styled' import ProjectFilesListItem from './ProjectFilesListItem' export type ProjectFileScript = { @@ -49,24 +44,17 @@ export default function ProjectFileList({ ) return ( - - - - - Cypher files - - - {sortedScripts.map(script => ( - - ))} - - - + + Cypher files + {sortedScripts.map(script => ( + + ))} + ) } diff --git a/src/browser/components/ProjectFiles/ProjectFilesListItem.tsx b/src/browser/components/ProjectFiles/ProjectFilesListItem.tsx index a11de41bf5b..c78cba19f5a 100644 --- a/src/browser/components/ProjectFiles/ProjectFilesListItem.tsx +++ b/src/browser/components/ProjectFiles/ProjectFilesListItem.tsx @@ -45,15 +45,14 @@ function ProjectFilesListItem({ }: ProjectFilesListItemProps): JSX.Element { const [isEditing, setIsEditing] = useState(false) return ( - + selectScript(script)} > {script.filename} - + {isEditing ? ( removeScript(script)} /> ) : ( diff --git a/src/browser/components/ProjectFiles/ProjectsFilesScripts.tsx b/src/browser/components/ProjectFiles/ProjectsFilesScripts.tsx index 42acce1f556..8aa81e48884 100644 --- a/src/browser/components/ProjectFiles/ProjectsFilesScripts.tsx +++ b/src/browser/components/ProjectFiles/ProjectsFilesScripts.tsx @@ -19,7 +19,7 @@ import { useEffect, useState, Dispatch } from 'react' import { Action } from 'redux' import { connect } from 'react-redux' import { useQuery, useMutation, ApolloError } from '@apollo/client' -import { filter, size } from 'lodash-es' +import { flatMap } from 'lodash-es' import * as editor from 'shared/modules/editor/editorDuck' import { @@ -39,7 +39,6 @@ import { DELETE_PROJECT_FILE, REMOVE_PROJECT_FILE } from './projectFilesConstants' -import Render from 'browser-components/Render' import { Bus } from 'suber' import { StyledErrorListContainer } from '../../modules/Sidebar/styled' import { withBus } from 'react-suber' @@ -49,53 +48,22 @@ import ProjectFileList, { interface ProjectFilesError { apolloErrors: (ApolloError | undefined)[] - errors?: string[] } export const ProjectFilesError = ({ - apolloErrors, - errors + apolloErrors }: ProjectFilesError): JSX.Element => { - const definedApolloErrors = filter( + const hasNetworkError = !!apolloErrors.find(error => error?.networkError) + const graphQLErrors = flatMap( apolloErrors, - apolloError => apolloError !== undefined - ) - const definedErrors = filter(errors, error => error !== '') - const ErrorsList = () => { - let errorsList: JSX.Element[] = [] - if (size(definedErrors)) { - errorsList = [ - ...errorsList, - ...definedErrors.map((definedError, i) => ( - {definedError} - )) - ] - } - if (size(definedApolloErrors)) { - definedApolloErrors.map((definedApolloError, j) => { - if (definedApolloError?.graphQLErrors.length) { - errorsList = [ - ...errorsList, - ...definedApolloError.graphQLErrors.map(({ message }, k) => ( - {message} - )) - ] - } - if (definedApolloError?.networkError) { - errorsList = [ - ...errorsList, - A network error has occurred. - ] - } - }) - } - return errorsList - } + error => error?.graphQLErrors.map(e => e.message) || [] + ).join('\n') return ( - - {ErrorsList()} - + + {graphQLErrors &&
{graphQLErrors}
} + {hasNetworkError &&
A network error has occurred
} +
) } diff --git a/src/browser/components/ProjectFiles/projectFilesConstants.ts b/src/browser/components/ProjectFiles/projectFilesConstants.ts index 0985a8de066..9b64b591ac5 100644 --- a/src/browser/components/ProjectFiles/projectFilesConstants.ts +++ b/src/browser/components/ProjectFiles/projectFilesConstants.ts @@ -19,36 +19,6 @@ import { gql } from '@apollo/client' export const REMOVE_PROJECT_FILE = 'REMOVE_PROJECT_FILE' -export interface ProhibitedFilenameErrors { - chars: string[] - tests: RegExp[] -} - -interface ProhibitedFilenamePlatform { - platform?: string -} - -interface ProhibitedFilenameChar extends ProhibitedFilenamePlatform { - char: string - test?: never -} - -interface ProhibitedFilenameTest extends ProhibitedFilenamePlatform { - test: RegExp - char?: never -} - -export const PROHIBITED_FILENAME_CHAR_TESTS: ( - | ProhibitedFilenameChar - | ProhibitedFilenameTest -)[] = [ - { char: '/' }, - { char: '\\' }, - { char: '..' }, - { test: /^[.|?]/ }, - { char: ':', platform: 'Win32' } -] - export interface AddProjectFile { addProjectFile: ProjectFile } diff --git a/src/browser/components/ProjectFiles/projectFilesUtils.ts b/src/browser/components/ProjectFiles/projectFilesUtils.ts index b59efcade46..21f7512d968 100644 --- a/src/browser/components/ProjectFiles/projectFilesUtils.ts +++ b/src/browser/components/ProjectFiles/projectFilesUtils.ts @@ -16,7 +16,6 @@ */ import remote from 'services/remote' import { ProjectFileScript } from 'browser-components/ProjectFiles/ProjectFilesList' -import { split, trim, head, startsWith } from 'lodash-es' import { ApolloCache, FetchResult, @@ -30,11 +29,10 @@ import { GET_PROJECT_FILES, AddProjectFile, RemoveProjectFile, - ProjectFileMapping, - PROHIBITED_FILENAME_CHAR_TESTS, - ProhibitedFilenameErrors + ProjectFileMapping } from './projectFilesConstants' import { CYPHER_FILE_EXTENSION } from 'services/exporting/favoriteUtils' +import { defaultNameFromDisplayContent } from 'browser-components/SavedScripts' export const ProjectFilesQueryVars = ( projectId: string @@ -80,95 +78,22 @@ const getProjectFileContents = ( 'X-API-Token': apiToken, 'X-Client-Id': clientId }) - .then(body => body.text()) // currently cypher/text file specific + .then(body => body.text()) .catch(e => { console.log(`Unable to get file ${name}\n`, e) return '' }) -const NEW_LINE = '\n' -const COMMENT_PREFIX = '//' +export const getProjectFileDefaultFileName = (contents: string): string => { + const defaultScriptName = defaultNameFromDisplayContent(contents) -const isNonEmptyString = (toTest: string): boolean => { - return typeof toTest === 'string' && Boolean(toTest) -} - -// adapted from @relate-by-ui/saved-scripts utils -export const setProjectFileDefaultFileName = (contents: string): string => { - const lines = split(contents, NEW_LINE) - const firstLine = trim(head(lines) || '') - - if (!isNonEmptyString(firstLine)) { - return '' - } - - // remove comment lines (if they exist) - const firstLineStr = startsWith(firstLine, COMMENT_PREFIX) - ? trim(firstLine.slice(COMMENT_PREFIX.length)) - : firstLine - - // this should be ok but may need adjusting - // if scenarios come up that require it - return firstLineStr + return defaultScriptName .replace(/\/|\\/g, '') // replace any forward or back slashes .replace(/[^\w]/g, '-') // replace any non-word chars with dashes .replace(/-+/g, '-') // replace 1 or more dashes with single dash .replace(/-$/, '') // remove dash from end of line } -export const createFilePath = (paths: string[]): string => { - return paths.join(/Win32/.test(navigator.platform) ? '\\' : '/') -} - -export const checkFileNameInput = (fileName: string): string => { - if (!fileName.length) { - return 'File name cannot be empty' - } - - const errors: ProhibitedFilenameErrors = { chars: [], tests: [] } - - PROHIBITED_FILENAME_CHAR_TESTS.forEach(charObj => { - if (charObj.test) { - // if a Regex test exists - if (charObj.platform) { - if ( - new RegExp(charObj.platform).test(navigator.platform) && - charObj.test.test(fileName) - ) { - errors.tests.push(charObj.test) - } - } else { - if (charObj.test.test(fileName)) { - errors.tests.push(charObj.test) - } - } - } - if (charObj.char) { - // otherwise default to .includes check - if (charObj.platform) { - if ( - new RegExp(charObj.platform).test(navigator.platform) && - fileName.includes(charObj.char) - ) { - errors.chars.push(charObj.char) - } - } else { - if (fileName.includes(charObj.char)) { - errors.chars.push(charObj.char) - } - } - } - }) - - return errors.chars.length || errors.tests.length - ? `File name cannot contain ${ - errors.chars.length ? `${errors.chars.join(', ')}` : '' - }${errors.chars.length && errors.tests.length ? ' or ' : ''}${ - errors.tests.length ? `${errors.tests.join(', ')}` : '' - }` - : '' -} - const readCacheQuery = ( cache: ApolloCache, projectId: string diff --git a/src/browser/components/SavedScripts/SavedScripts.tsx b/src/browser/components/SavedScripts/SavedScripts.tsx index 287cb8b0765..3eb8e092853 100644 --- a/src/browser/components/SavedScripts/SavedScripts.tsx +++ b/src/browser/components/SavedScripts/SavedScripts.tsx @@ -17,23 +17,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React from 'react' +import React, { useState } from 'react' import { DndProvider } from 'react-dnd' -import HTML5Backend from 'react-dnd-html5-backend' +import { HTML5Backend } from 'react-dnd-html5-backend' import SavedScriptsFolder from './SavedScriptsFolder' -import { ExportButton, NewFolderButton } from './SavedScriptsButton' import { - SavedScriptsMain, + ExportButton, + NewFolderButton, + RedRemoveButton +} from './SavedScriptsButton' +import { SavedScriptsBody, - SavedScriptsBodySection, SavedScriptsHeader, SavedScriptsButtonWrapper, - SavedScriptsListItemDisplayName + SavedScriptsNewFavorite } from './styled' -import { Favorite } from 'shared/modules/favorites/favoritesDuck' import { Folder } from 'shared/modules/favorites/foldersDuck' import SavedScriptsListItem from './SavedScriptsListItem' import { getScriptDisplayName } from './utils' +import { uniq } from 'lodash-es' +import { Favorite } from 'shared/modules/favorites/favoritesDuck' +import { useCustomBlur } from './hooks' +import { AddIcon } from 'browser-components/icons/Icons' +import uuid from 'uuid' interface SavedScriptsProps { title?: string @@ -45,22 +51,34 @@ interface SavedScriptsProps { exportScripts?: (scripts: Favorite[], folders: Folder[]) => void renameScript?: (script: Favorite, name: string) => void moveScript?: (scriptId: string, folderId: string) => void - removeScript?: (script: Favorite) => void - renameFolder?: (folder: Folder, name: string) => void - removeFolder?: (folder: Folder) => void - createNewFolder?: () => void + addScript?: (content: string) => void + removeScripts?: (scripts: string[]) => void + renameFolder?: (folderId: string, name: string) => void + removeFolder?: (folderId: string) => void + createNewFolder?: (id?: string) => void createNewScript?: () => void } +function findScriptsFromIds(ids: string[], scripts: Favorite[]): Favorite[] { + function notEmpty(value?: T): value is T { + return value !== undefined + } + + return ids + .map(id => scripts.find(script => getUniqueScriptKey(script) === id)) + .filter(notEmpty) +} + export default function SavedScripts({ - title = 'Saved Scripts', + title = 'Local Scripts', scripts, folders, selectScript, createNewScript, execScript, renameScript, - removeScript, + removeScripts, + addScript, moveScript, renameFolder, removeFolder, @@ -78,67 +96,164 @@ export default function SavedScripts({ .sort(sortScriptsAlfabethically) })) + const [unNamedFolder, setUnNamedFolder] = useState(null) + const [selectedIds, setSelectedIds] = useState([]) + const blurRef = useCustomBlur(() => setSelectedIds([])) + + const onListItemClick = (clickedScriptId: string) => ( + e: React.MouseEvent + ) => { + const toggleFn = (ids: string[]) => + ids.includes(clickedScriptId) + ? ids.filter(existingId => existingId !== clickedScriptId) + : ids.concat(clickedScriptId) + + const getIdRange = (id1: string, id2: string): string[] => { + const scriptIds: string[] = scripts + .concat([]) // to avoid mutating in place by sort + .sort(sortScriptsAlfabethically) + .map(getUniqueScriptKey) + const pos1 = scriptIds.indexOf(id1) + const pos2 = scriptIds.indexOf(id2) + if (pos1 === -1 || pos2 == -1) { + throw new Error("Can't get range between ids not in list") + } + + const smallestFirst = pos1 < pos2 ? [pos1, pos2] : [pos2, pos1] + return scriptIds.slice( + smallestFirst[0], + smallestFirst[1] + 1 /* inclusive slice */ + ) + } + + const manualMultiselect = e.metaKey || e.ctrlKey + const bulkSelect = e.shiftKey + if (bulkSelect) { + setSelectedIds(ids => + ids.length === 0 + ? [clickedScriptId] + : uniq([...ids, ...getIdRange(ids[ids.length - 1], clickedScriptId)]) + ) + } else if (manualMultiselect) { + setSelectedIds(toggleFn) + } else { + setSelectedIds([clickedScriptId]) + } + } + + const selectedScripts = findScriptsFromIds(selectedIds, scripts) + const removeScript = + removeScripts && ((id: string) => () => removeScripts([id])) + const hasSelectedIds = !!selectedScripts.length + const newFolderButton = createNewFolder && ( + { + const id = uuid.v4() + setUnNamedFolder(id) + createNewFolder(id) + }} + /> + ) return ( - - - - - - {title} - - {exportScripts && ( - exportScripts(scripts, folders)} - /> - )} - {createNewFolder && ( - - )} + + + + {title} + {hasSelectedIds ? ( + <> + | + + + + {selectedIds.length} selected{' '} + + {exportScripts && ( + { + exportScripts(selectedScripts, folders) + setSelectedIds([]) + }} + /> + )} + {removeScripts && ( + { + removeScripts(selectedIds) + setSelectedIds([]) + }} + /> + )} + + {newFolderButton} - + + ) : ( + newFolderButton + )} + - {scriptsOutsideFolder.map(script => ( - - ))} - {createNewScript && ( - - Create new favorite - - )} - {foldersWithScripts.map(({ folder, scripts }) => ( - - {scripts.map(script => ( - - ))} - - ))} - - - - + {scriptsOutsideFolder.map(script => { + const key = getUniqueScriptKey(script) + return ( + selectScript(script)} + execScript={() => execScript(script)} + duplicateScript={() => addScript && addScript(script.content)} + removeScript={removeScript && removeScript(key)} + renameScript={(name: string) => + renameScript && renameScript(script, name) + } + script={script} + key={key} + onClick={onListItemClick(key)} + isSelected={selectedIds.includes(key)} + /> + ) + })} + {foldersWithScripts.map(({ folder, scripts }) => ( + setUnNamedFolder(null)} + > + {scripts.map(script => { + const key = getUniqueScriptKey(script) + return ( + selectScript(script)} + execScript={() => execScript(script)} + duplicateScript={ + addScript && (() => addScript(script.content)) + } + removeScript={removeScript && removeScript(key)} + renameScript={ + renameScript && + ((name: string) => renameScript(script, name)) + } + script={script} + key={key} + onClick={onListItemClick(key)} + isSelected={selectedIds.includes(key)} + /> + ) + })} + + ))} + {createNewScript && ( + + Add empty favorite + + )} + + ) } diff --git a/src/browser/components/SavedScripts/SavedScriptsButton.tsx b/src/browser/components/SavedScripts/SavedScriptsButton.tsx index 2197a0de714..6e04dbf2427 100644 --- a/src/browser/components/SavedScripts/SavedScriptsButton.tsx +++ b/src/browser/components/SavedScripts/SavedScriptsButton.tsx @@ -21,6 +21,10 @@ import React, { ReactEventHandler } from 'react' import { Icon, SemanticICONS } from 'semantic-ui-react' import { SemanticCOLORS } from 'semantic-ui-react/dist/commonjs/generic' import { StyledSavedScriptsButton } from './styled' +import SVGInline from 'react-svg-inline' +import newFolderIcon from 'icons/folder-add.svg' +import hollow_run_icon from 'icons/hollow-run-icon.svg' +import { DownloadIcon } from 'browser-components/icons/Icons' type SavedScriptsButtonProps = { onClick: ReactEventHandler @@ -37,7 +41,6 @@ export default function SavedScriptsButton({ }: SavedScriptsButtonProps): JSX.Element { return ( - SavedScriptsButton({ onClick, title: 'Export', iconName: 'download' }) const EditButton = ({ onClick }: OnClickProp): JSX.Element => SavedScriptsButton({ onClick, title: 'Edit', iconName: 'pencil' }) -const RunButton = ({ onClick }: OnClickProp): JSX.Element => - SavedScriptsButton({ onClick, title: 'Run', iconName: 'play' }) +const ExportButton = ({ onClick }: OnClickProp): JSX.Element => ( + + + +) -const NewFolderButton = ({ onClick }: OnClickProp): JSX.Element => - SavedScriptsButton({ - onClick, - title: 'New folder', - iconName: 'folder open outline' - }) +const RunButton = ({ onClick }: OnClickProp): JSX.Element => ( + + + +) +const NewFolderButton = ({ onClick }: OnClickProp): JSX.Element => ( + + + +) const RemoveButton = ({ onClick }: OnClickProp): JSX.Element => SavedScriptsButton({ diff --git a/src/browser/components/SavedScripts/SavedScriptsFolder.tsx b/src/browser/components/SavedScripts/SavedScriptsFolder.tsx index 7ba5e833867..e1694f2c615 100644 --- a/src/browser/components/SavedScripts/SavedScriptsFolder.tsx +++ b/src/browser/components/SavedScripts/SavedScriptsFolder.tsx @@ -21,27 +21,36 @@ import React, { useState } from 'react' import { useDrop } from 'react-dnd' import { useCustomBlur, useNameUpdate } from './hooks' -import { EditButton, RemoveButton } from './SavedScriptsButton' import { + NavIcon, + FolderIcon, SavedScriptsCollapseMenuIcon, SavedScriptsExpandMenuRightIcon } from 'browser-components/icons/Icons' import { SavedScriptsButtonWrapper, - SavedScriptsFolderCollapseIcon, SavedScriptsFolderHeader, SavedScriptsFolderLabel, SavedScriptsFolderMain, - SavedScriptsInput + SavedScriptsInput, + ChildrenContainer, + FolderNameWrapper, + ContextMenuHoverParent, + ContextMenu, + ContextMenuContainer, + ContextMenuItem } from './styled' import { Folder } from 'shared/modules/favorites/foldersDuck' interface SavedScriptsFolderProps { folder: Folder - renameFolder?: (folder: Folder, name: string) => void - removeFolder?: (folder: Folder) => void + renameFolder?: (folderId: string, name: string) => void + removeFolder?: (folderId: string) => void moveScript?: (scriptId: string, folderId: string) => void + forceEdit: boolean + onDoneEditing: () => void + selectedScriptIds: string[] children: JSX.Element[] } @@ -50,6 +59,9 @@ function SavedScriptsFolder({ moveScript, renameFolder, removeFolder, + selectedScriptIds, + forceEdit, + onDoneEditing, children }: SavedScriptsFolderProps): JSX.Element { const { @@ -58,11 +70,14 @@ function SavedScriptsFolder({ beginEditing, doneEditing, setNameValue - } = useNameUpdate( - folder.name, - () => renameFolder && renameFolder(folder, currentNameValue) - ) - const blurRef = useCustomBlur(doneEditing) + } = useNameUpdate(folder.name, () => { + renameFolder && renameFolder(folder.id, currentNameValue) + onDoneEditing() + }) + const blurRef = useCustomBlur(() => { + doneEditing() + onDoneEditing() + }) const [expanded, setExpanded] = useState(false) const drop = useDrop< { id: string; type: string }, @@ -71,58 +86,90 @@ function SavedScriptsFolder({ >({ accept: 'script', drop: item => { - moveScript && moveScript(item.id, folder.id) + if (moveScript) { + // move dragged + moveScript(item.id, folder.id) + // Also move all selected + selectedScriptIds.forEach(id => moveScript(id, folder.id)) + } } })[1] + const [showOverlay, setShowOverlay] = useState(false) + const overlayBlurRef = useCustomBlur(() => setShowOverlay(false)) + const toggleOverlay = () => setShowOverlay(t => !t) + const contextMenuContent = [ + renameFolder && ( + + Rename folder + + ), + removeFolder && ( + removeFolder(folder.id)} + > + Delete folder + + ) + ].filter(defined => defined) + return ( -
- - - {isEditing ? ( + + + + {isEditing || forceEdit ? ( { - key === 'Enter' && doneEditing() + if (key === 'Enter') { + doneEditing() + onDoneEditing() + } }} + onFocus={event => event.target.select()} value={currentNameValue} onChange={e => setNameValue(e.target.value)} data-testid="editSavedScriptFolderName" /> ) : ( setExpanded(!expanded)} > - - {expanded ? ( - - ) : ( - - )} - - {folder.name} + {expanded ? ( + + ) : ( + + )} + + {folder.name} )} - - {removeFolder && isEditing && ( - removeFolder(folder)} /> - )} - {renameFolder && !isEditing && ( - + + {contextMenuContent.length > 0 && ( + + + {showOverlay && ( + + {contextMenuContent} + + )} + )} - {expanded && children} + {expanded && children} -
+ ) } diff --git a/src/browser/components/SavedScripts/SavedScriptsListItem.tsx b/src/browser/components/SavedScripts/SavedScriptsListItem.tsx index df3a7137c99..019297752d1 100644 --- a/src/browser/components/SavedScripts/SavedScriptsListItem.tsx +++ b/src/browser/components/SavedScripts/SavedScriptsListItem.tsx @@ -17,25 +17,35 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React from 'react' + +import React, { useState } from 'react' import { useDrag } from 'react-dnd' import { useCustomBlur, useNameUpdate } from './hooks' -import { RemoveButton, RunButton, EditButton } from './SavedScriptsButton' +import { RunButton } from './SavedScriptsButton' import { SavedScriptsButtonWrapper, SavedScriptsInput, SavedScriptsListItemDisplayName, - SavedScriptsListItemMain + SavedScriptsListItemMain, + ContextMenuHoverParent, + ContextMenu, + ContextMenuContainer, + ContextMenuItem, + Separator } from './styled' import { Favorite } from 'shared/modules/favorites/favoritesDuck' import { getScriptDisplayName } from './utils' +import { NavIcon } from 'browser-components/icons/Icons' interface SavedScriptsListItemProps { script: Favorite - selectScript: (script: Favorite) => void - execScript: (script: Favorite) => void - renameScript?: (script: Favorite, name: string) => void - removeScript?: (script: Favorite) => void + selectScript: () => void + execScript: () => void + onClick?: (e: React.MouseEvent) => void + isSelected?: boolean + renameScript?: (name: string) => void + removeScript?: () => void + duplicateScript?: () => void } function SavedScriptsListItem({ @@ -43,7 +53,10 @@ function SavedScriptsListItem({ selectScript, execScript, renameScript, - removeScript + removeScript, + duplicateScript, + onClick, + isSelected }: SavedScriptsListItemProps): JSX.Element { const displayName = getScriptDisplayName(script) const { @@ -54,46 +67,99 @@ function SavedScriptsListItem({ setNameValue } = useNameUpdate( displayName, - () => renameScript && renameScript(script, currentNameValue) + () => renameScript && renameScript(currentNameValue) ) - const blurRef = useCustomBlur(doneEditing) - const drag = useDrag({ + const overlayBlurRef = useCustomBlur(() => setShowOverlay(false)) + const dragAndDropRef = useDrag({ item: { id: script.id, type: 'script' } })[1] - + const [showOverlay, setShowOverlay] = useState(false) + const toggleOverlay = () => setShowOverlay(t => !t) const canRunScript = !script.not_executable && !isEditing return ( - - {isEditing ? ( - { - key === 'Enter' && doneEditing() - }} - value={currentNameValue} - onChange={e => setNameValue(e.target.value)} - /> - ) : ( - !isEditing && selectScript(script)} - ref={drag} - > - {displayName} - - )} - - {removeScript && isEditing && ( - removeScript(script)} /> + + + {isEditing ? ( + { + key === 'Enter' && doneEditing() + }} + onBlur={doneEditing} + value={currentNameValue} + onChange={e => setNameValue(e.target.value)} + /> + ) : ( + script.isStatic && selectScript()} + > + {displayName} + )} - {renameScript && !isEditing && } - {canRunScript && execScript(script)} />} - - + + + + {showOverlay && ( + + {removeScript && ( + + Delete + + )} + {removeScript && } + {renameScript && ( + + Rename + + )} + { + + Edit content + + } + {canRunScript && ( + + Run + + )} + {duplicateScript && ( + + Duplicate + + )} + + )} + + {canRunScript && } + +
+ ) } diff --git a/src/browser/components/SavedScripts/styled.ts b/src/browser/components/SavedScripts/styled.ts index db7d6fb7d41..08862a60940 100644 --- a/src/browser/components/SavedScripts/styled.ts +++ b/src/browser/components/SavedScripts/styled.ts @@ -1,13 +1,8 @@ import styled from 'styled-components' -export const SavedScriptsMain = styled.div`` - export const SavedScriptsBody = styled.div` - padding: 0 24px; -` - -export const SavedScriptsBodySection = styled.div` - margin-bottom: 12px; + padding: 0 18px; + margin-bottom: 25px; ` export const SavedScriptsHeader = styled.h5` @@ -19,21 +14,45 @@ export const SavedScriptsHeader = styled.h5` line-height: 39px; position: relative; font-weight: bold; - -webkit-font-smoothing: antialiased; - text-shadow: rgba(0, 0, 0, 0.4) 0px 1px 0px; +` +export const FolderNameWrapper = styled.span` + margin-left: 5px; ` -export const SavedScriptsListItemMain = styled.div` - padding: 5px 3px; +export const SavedScriptsListItemMain = styled.div<{ + isSelected?: boolean +}>` + padding: 5px; display: flex; justify-content: space-between; + background-color: ${props => + props.isSelected ? props.theme.hoverBackground : 'inherit'}; + + ${props => + props.isSelected + ? `margin-left: -3px; +border-left: 3px solid #68BDF4;` + : ''}; + &:hover { color: inherit; + background-color: ${props => props.theme.hoverBackground}; } +` - &:hover .saved-scripts__edit-button { - visibility: visible; +export const SavedScriptsNewFavorite = styled.div` + flex: 1; + user-select: none; + cursor: pointer; + color: #bcc0c9; + font-size: 13px; + margin-left: 6px; + margin-top: 10px; + transition: color ease-in-out 0.3s; + + &:hover { + color: inherit; } ` @@ -48,25 +67,19 @@ export const SavedScriptsListItemDisplayName = styled.div` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - transition: color ease-in-out 0.3s; - - &:hover { - color: inherit; - } ` export const SavedScriptsFolderMain = styled.div` - padding-bottom: 16px; + padding: 5px; +` +export const ChildrenContainer = styled.div` + padding-left: 10px; ` export const SavedScriptsFolderHeader = styled.div` display: flex; justify-content: space-between; padding-bottom: 5px; - - &:hover .saved-scripts__edit-button { - visibility: visible; - } ` export const SavedScriptsFolderBody = styled.div` @@ -78,16 +91,17 @@ export const SavedScriptsFolderLabel = styled.div` margin-right: 10px; user-select: none; cursor: pointer; - font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + display: flex; ` export const SavedScriptsFolderCollapseIcon = styled.span` - margin-right: 10px; + margin-right: 3px; width: 8px; display: inline-block; + vertical-align: middle; ` export const SavedScriptsButtonWrapper = styled.div` @@ -119,4 +133,54 @@ export const SavedScriptsInput = styled.input` background: transparent; font-weight: normal; margin-right: 5px; + + ::selection { + color: white; + background-color: ${props => props.theme.linkHover}; + } +` + +export const ContextMenuContainer = styled.span` + position: relative; +` + +export const ContextMenuHoverParent = styled.span<{ stayVisible?: boolean }>` + ${ContextMenuContainer} { + visibility: ${props => (props.stayVisible ? 'visible' : 'hidden')}; + } + + &:hover ${ContextMenuContainer} { + visibility: visible; + } +` + +export const ContextMenu = styled.div` + color: ${props => props.theme.primaryText}; + padding-top: 5px; + padding-bottom: 5px; + position: absolute; + width: 156px; + left: -156px; + top: -3px; + z-index: 999; + border: 1px solid transparent; + background-color: ${props => props.theme.secondaryBackground}; + border: ${props => props.theme.frameBorder}; + + box-shadow: 0px 0px 2px rgba(52, 58, 67, 0.1), + 0px 1px 2px rgba(52, 58, 67, 0.08), 0px 1px 4px rgba(52, 58, 67, 0.08); + border-radius: 2px; +` +export const ContextMenuItem = styled.div` + cursor: pointer; + width: 100%; + padding-left: 5px; + + &:hover { + background-color: ${props => props.theme.primaryBackground}; + } +` + +export const Separator = styled.div` + border-bottom: 1px solid rgb(77, 74, 87, 0.3); ` diff --git a/src/browser/components/icons/Icons.tsx b/src/browser/components/icons/Icons.tsx index 999ad7496f1..26ee681a8e3 100644 --- a/src/browser/components/icons/Icons.tsx +++ b/src/browser/components/icons/Icons.tsx @@ -45,6 +45,10 @@ import save_favorite from 'icons/save_favorite.svg' import run_icon from 'icons/run_icon.svg' import stop_icon from 'icons/stop_icon.svg' import help from 'icons/help.svg' +import newFolder from 'icons/folder-add.svg' +import folder from 'icons/folder-empty.svg' +import addCircle from 'icons/add-circle.svg' +import navIcon from 'icons/navigation-menu-vertical.svg' import cannyFeedback from 'icons/canny-feedback.svg' import cannyNotifications from 'icons/canny-changelog.svg' @@ -374,6 +378,7 @@ export const QuestionIcon = ({ title }: { title: string }): JSX.Element => ( className="fa fa-question-circle-o" /> ) + export const PlusIcon = (): JSX.Element => ( ) +export const NewFolderIcon = () => +export const NavIcon = () => +export const AddIcon = () => +export const FolderIcon = () => +export const SavedScriptsPlay = () => ( + +) + export const SavedScriptsExpandMenuRightIcon = (): JSX.Element => ( ) diff --git a/src/browser/icons/add-circle.svg b/src/browser/icons/add-circle.svg new file mode 100644 index 00000000000..2ff0bf275d1 --- /dev/null +++ b/src/browser/icons/add-circle.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/browser/icons/app-window-code.svg b/src/browser/icons/app-window-code.svg index e0fbd035a97..b6bba9d86f5 100644 --- a/src/browser/icons/app-window-code.svg +++ b/src/browser/icons/app-window-code.svg @@ -10,7 +10,6 @@ } - app-window-code diff --git a/src/browser/icons/folder-add.svg b/src/browser/icons/folder-add.svg new file mode 100644 index 00000000000..680bf5889c6 --- /dev/null +++ b/src/browser/icons/folder-add.svg @@ -0,0 +1,20 @@ + + + folder-add + + + + + + \ No newline at end of file diff --git a/src/browser/icons/folder-empty.svg b/src/browser/icons/folder-empty.svg new file mode 100644 index 00000000000..2bee36ea8fa --- /dev/null +++ b/src/browser/icons/folder-empty.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/browser/icons/hollow-run-icon.svg b/src/browser/icons/hollow-run-icon.svg new file mode 100644 index 00000000000..d0cb8e5d0fa --- /dev/null +++ b/src/browser/icons/hollow-run-icon.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/browser/icons/navigation-menu-vertical.svg b/src/browser/icons/navigation-menu-vertical.svg new file mode 100644 index 00000000000..d2809d3fd0c --- /dev/null +++ b/src/browser/icons/navigation-menu-vertical.svg @@ -0,0 +1,15 @@ + + + navigation-menu-vertical + + + + + + \ No newline at end of file diff --git a/src/browser/init.ts b/src/browser/init.ts index 8e29fc2ad1e..0295669fc1b 100644 --- a/src/browser/init.ts +++ b/src/browser/init.ts @@ -25,6 +25,7 @@ import './styles/neo4j-world.css' import './styles/font-awesome.min.css' import './styles/fira-code.css' import './styles/open-sans.css' +import './styles/util-classes.css' import '@relate-by-ui/css/semantic/dist/relate-by.min.css' // non web env (just for tests) diff --git a/src/browser/modules/App/App.tsx b/src/browser/modules/App/App.tsx index f6e666a9d4b..a1fba9d8a15 100644 --- a/src/browser/modules/App/App.tsx +++ b/src/browser/modules/App/App.tsx @@ -136,10 +136,8 @@ export function App(props: any) { databases } = props - const wrapperClassNames = [] - if (!codeFontLigatures) { - wrapperClassNames.push('disable-font-ligatures') - } + const wrapperClassNames = codeFontLigatures ? '' : 'disable-font-ligatures' + const setEventMetricsCallback = (fn: any) => { eventMetricsCallback.current = fn } @@ -175,7 +173,6 @@ export function App(props: any) { - {/* @ts-expect-error ts-migrate(2769) FIXME: Type 'string[]' is not assignable to type 'string'... Remove this comment to see the full error message */} diff --git a/src/browser/modules/Editor/EditorFrame.tsx b/src/browser/modules/Editor/EditorFrame.tsx index 162fb2ab9cf..72c5366748a 100644 --- a/src/browser/modules/Editor/EditorFrame.tsx +++ b/src/browser/modules/Editor/EditorFrame.tsx @@ -18,13 +18,7 @@ * along with this program. If not, see . */ -import React, { - useState, - Dispatch, - useEffect, - useRef, - useCallback -} from 'react' +import React, { useState, Dispatch, useEffect, useRef } from 'react' import { Action } from 'redux' import SVGInline from 'react-svg-inline' import { connect } from 'react-redux' @@ -73,7 +67,7 @@ import { ADD_PROJECT_FILE, REMOVE_PROJECT_FILE } from 'browser-components/ProjectFiles/projectFilesConstants' -import { setProjectFileDefaultFileName } from 'browser-components/ProjectFiles/projectFilesUtils' +import { getProjectFileDefaultFileName } from 'browser-components/ProjectFiles/projectFilesUtils' import Monaco, { MonacoHandles } from './Monaco' import { codeFontLigatures, @@ -230,7 +224,7 @@ export function EditorFrame({ return name } if (isProjectFile) { - return setProjectFileDefaultFileName(content) + return getProjectFileDefaultFileName(content) } return defaultNameFromDisplayContent(content) @@ -290,7 +284,6 @@ export function EditorFrame({ variables: { projectId, fileUpload: new File([editorValue], name), - destination: `./${name}`, overwrite: true } }) diff --git a/src/browser/modules/Frame/FrameTitlebar.tsx b/src/browser/modules/Frame/FrameTitlebar.tsx index 4ce36c4c566..286f57c5855 100644 --- a/src/browser/modules/Frame/FrameTitlebar.tsx +++ b/src/browser/modules/Frame/FrameTitlebar.tsx @@ -92,6 +92,8 @@ import Monaco, { MonacoHandles } from '../Editor/Monaco' import { Bus } from 'suber' import FeatureToggle from '../FeatureToggle/FeatureToggle' import { reusableFrame } from 'shared/modules/experimentalFeatures/experimentalFeaturesDuck' +import { addFavorite } from 'shared/modules/favorites/favoritesDuck' +import uuid from 'uuid' type FrameTitleBarBaseProps = { frame: any @@ -449,7 +451,8 @@ const mapDispatchToProps = ( ) => { return { newFavorite: (cmd: string) => { - dispatch(sidebar.setDraftScript(cmd, 'favorites')) + dispatch(addFavorite(cmd)) + dispatch(sidebar.open('favorites')) }, newProjectFile: (cmd: string) => { dispatch(sidebar.setDraftScript(cmd, 'project files')) diff --git a/src/browser/modules/Sidebar/NewSavedScript.tsx b/src/browser/modules/Sidebar/NewSavedScript.tsx index 1eb95a59433..c43c6a55d35 100644 --- a/src/browser/modules/Sidebar/NewSavedScript.tsx +++ b/src/browser/modules/Sidebar/NewSavedScript.tsx @@ -63,13 +63,17 @@ interface NewSavedScriptProps { onCancel: () => void defaultName: string headerText: string + pattern?: string + patternMessage?: string } function NewSavedScript({ onSubmit, defaultName, headerText, - onCancel + onCancel, + patternMessage = '', + pattern }: NewSavedScriptProps): JSX.Element { const [name, setName] = useState(defaultName) @@ -89,6 +93,8 @@ function NewSavedScript({ data-testid="scriptName" value={name} onChange={onChange} + pattern={pattern} + title={patternMessage} required /> diff --git a/src/browser/modules/Sidebar/ProjectFiles.tsx b/src/browser/modules/Sidebar/ProjectFiles.tsx index 40db1cc8c9f..ed3a007a65a 100644 --- a/src/browser/modules/Sidebar/ProjectFiles.tsx +++ b/src/browser/modules/Sidebar/ProjectFiles.tsx @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import React, { useState, Dispatch } from 'react' +import React, { Dispatch } from 'react' import { Action } from 'redux' import { connect } from 'react-redux' import { useMutation } from '@apollo/client' @@ -29,9 +29,8 @@ import ProjectFilesScripts, { } from '../../components/ProjectFiles/ProjectsFilesScripts' import NewSavedScript from './NewSavedScript' import { - setProjectFileDefaultFileName, - updateCacheAddProjectFile, - checkFileNameInput + getProjectFileDefaultFileName, + updateCacheAddProjectFile } from '../../components/ProjectFiles/projectFilesUtils' import { ADD_PROJECT_FILE } from '../../components/ProjectFiles/projectFilesConstants' import { setDraftScript } from 'shared/modules/sidebar/sidebarDuck' @@ -49,17 +48,10 @@ const ProjectFiles = ({ resetDraft }: ProjectFilesProps) => { const [addFile, { error: apolloError }] = useMutation(ADD_PROJECT_FILE) - const [error, setError] = useState('') function save(inputedFileName: string) { const cypherFileExt = new RegExp(`${CYPHER_FILE_EXTENSION}$`) const fileName = inputedFileName.replace(cypherFileExt, '') - setError('') - - if (checkFileNameInput(fileName)) { - setError(checkFileNameInput(fileName)) - return - } addFile({ variables: { @@ -81,30 +73,28 @@ const ProjectFiles = ({ Project Files {scriptDraft && ( )} - + ) } -const mapStateToProps = (state: any) => { - return { - projectId: getProjectId(state) - } -} +const mapStateToProps = (state: any) => ({ + projectId: getProjectId(state) +}) -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - resetDraft: () => { - dispatch(setDraftScript(null, 'project files')) - } +const mapDispatchToProps = (dispatch: Dispatch) => ({ + resetDraft: () => { + dispatch(setDraftScript(null, 'project files')) } -} +}) export default connect(mapStateToProps, mapDispatchToProps)(ProjectFiles) diff --git a/src/browser/modules/Sidebar/Sidebar.tsx b/src/browser/modules/Sidebar/Sidebar.tsx index a461f455380..38b8dacab66 100644 --- a/src/browser/modules/Sidebar/Sidebar.tsx +++ b/src/browser/modules/Sidebar/Sidebar.tsx @@ -18,9 +18,7 @@ * along with this program. If not, see . */ -import React, { ReactFragment, ReactElement, Dispatch } from 'react' -import { Action } from 'redux' -import uuid from 'uuid' +import React, { ReactFragment, ReactElement } from 'react' import { connect } from 'react-redux' import DatabaseDrawer from '../DBMSInfo/DBMSInfo' import DocumentsDrawer from './Documents' @@ -32,22 +30,15 @@ import ProjectFilesDrawer from './ProjectFiles' import TabNavigation, { NavItem } from 'browser-components/TabNavigation/Navigation' -import { DrawerHeader } from 'browser-components/drawer' -import NewSavedScript from './NewSavedScript' import BrowserSync from '../Sync/BrowserSync' import { GlobalState } from 'shared/globalState' import { isUserSignedIn } from 'shared/modules/sync/syncDuck' -import { addFavorite } from 'shared/modules/favorites/favoritesDuck' import { utilizeBrowserSync } from 'shared/modules/features/featuresDuck' import { PENDING_STATE, CONNECTED_STATE, DISCONNECTED_STATE } from 'shared/modules/connections/connectionsDuck' -import { - getCurrentDraft, - setDraftScript -} from 'shared/modules/sidebar/sidebarDuck' import { isRelateAvailable } from 'shared/modules/app/appDuck' import { @@ -59,7 +50,8 @@ import { AboutIcon, ProjectFilesIcon } from 'browser-components/icons/Icons' -import { defaultNameFromDisplayContent } from 'browser-components/SavedScripts' +import { getCurrentDraft } from 'shared/modules/sidebar/sidebarDuck' +import { DrawerHeader } from 'browser-components/drawer' interface SidebarProps { openDrawer: string @@ -69,8 +61,6 @@ interface SidebarProps { syncConnected: boolean loadSync: boolean isRelateAvailable: boolean - addFavorite: (cmd: string) => void - resetDraft: () => void scriptDraft: string | null } @@ -82,9 +72,7 @@ const Sidebar = ({ syncConnected, loadSync, isRelateAvailable, - addFavorite, - scriptDraft, - resetDraft + scriptDraft }: SidebarProps) => { const topNavItemsList: NavItem[] = [ { @@ -110,32 +98,7 @@ const Sidebar = ({ content: function FavoritesDrawer(): ReactFragment { return ( <> - Favorites - {scriptDraft && ( - { - if (input === defaultNameFromDisplayContent(scriptDraft)) { - addFavorite(scriptDraft) - } else { - const alreadyHasName = scriptDraft.startsWith('//') - const replaceName = [ - `// ${input}`, - scriptDraft.split('\n').slice(1) - ].join('\n') - - addFavorite( - alreadyHasName - ? replaceName - : `//${input}\n${scriptDraft}` - ) - } - resetDraft() - }} - defaultName={defaultNameFromDisplayContent(scriptDraft)} - headerText={'Save as'} - onCancel={resetDraft} - /> - )} + Favorites {showStaticScripts && } @@ -235,15 +198,4 @@ const mapStateToProps = (state: GlobalState) => { } } -const mapDispatchToProps = (dispatch: Dispatch) => { - return { - addFavorite: (cmd: string) => { - dispatch(addFavorite(cmd, uuid.v4())) - }, - resetDraft: () => { - dispatch(setDraftScript(null, 'favorites')) - } - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Sidebar) +export default connect(mapStateToProps)(Sidebar) diff --git a/src/browser/modules/Sidebar/favorites.ts b/src/browser/modules/Sidebar/favorites.ts index 66efb11796d..500af0e1d9e 100644 --- a/src/browser/modules/Sidebar/favorites.ts +++ b/src/browser/modules/Sidebar/favorites.ts @@ -40,7 +40,7 @@ const mapFavoritesStateToProps = (state: any) => { return { folders, scripts, - title: 'Local Scripts' + title: 'Local scripts' } } @@ -54,8 +54,8 @@ const mapFavoritesDispatchToProps = (dispatch: any, ownProps: any) => ({ dispatch( executeCommand(favorite.content, { source: commandSources.favorite }) ), - removeScript: (favorite: favoritesDuck.Favorite) => - favorite.id && dispatch(favoritesDuck.removeFavorite(favorite.id)), + removeScripts: (ids: string[]) => + dispatch(favoritesDuck.removeFavorites(ids)), renameScript: (favorite: favoritesDuck.Favorite, name: string) => { if (favorite.id) { dispatch(favoritesDuck.renameFavorite(favorite.id, name)) @@ -64,8 +64,8 @@ const mapFavoritesDispatchToProps = (dispatch: any, ownProps: any) => ({ updateFolders(folders: foldersDuck.Folder[]) { dispatch(foldersDuck.updateFolders(folders)) }, - createNewFolder() { - dispatch(foldersDuck.addFolder(uuid.v4(), 'New Folder')) + createNewFolder(id?: string) { + dispatch(foldersDuck.addFolder(id || uuid.v4(), 'New Folder')) }, dispatchRemoveFolderAndItsScripts(folderId: string, favoriteIds: string[]) { dispatch(foldersDuck.removeFolder(folderId)) @@ -86,6 +86,9 @@ const mapFavoritesDispatchToProps = (dispatch: any, ownProps: any) => ({ folders: foldersDuck.Folder[] ) { exportFavorites(favorites, folders) + }, + addScript(content: string) { + dispatch(favoritesDuck.addFavorite(content)) } }) @@ -93,21 +96,18 @@ const mergeProps = (stateProps: any, dispatchProps: any) => { return { ...stateProps, ...dispatchProps, - renameFolder: (folderToRename: foldersDuck.Folder, name: string) => { + renameFolder: (folderId: string, name: string) => { dispatchProps.updateFolders( stateProps.folders.map((folder: foldersDuck.Folder) => - folderToRename.id === folder.id ? { ...folder, name } : folder + folderId === folder.id ? { ...folder, name } : folder ) ) }, - removeFolder(folder: foldersDuck.Folder) { + removeFolder(folderId: string) { const scriptsToRemove = stateProps.scripts - .filter((script: favoritesDuck.Favorite) => script.folder === folder.id) + .filter((script: favoritesDuck.Favorite) => script.folder === folderId) .map((script: favoritesDuck.Favorite) => script.id) - dispatchProps.dispatchRemoveFolderAndItsScripts( - folder.id, - scriptsToRemove - ) + dispatchProps.dispatchRemoveFolderAndItsScripts(folderId, scriptsToRemove) } } } diff --git a/src/browser/modules/Sidebar/styled.tsx b/src/browser/modules/Sidebar/styled.tsx index 83ee2756f63..b06f9c1fcf8 100644 --- a/src/browser/modules/Sidebar/styled.tsx +++ b/src/browser/modules/Sidebar/styled.tsx @@ -76,7 +76,7 @@ export const StyledCommandListItem = styled.li` position: relative; &:hover { - background-color: #40444e; + background-color: ${props => props.theme.hoverBackground}; &:after { content: ' '; diff --git a/src/browser/modules/Stream/SysInfoFrame/index.tsx b/src/browser/modules/Stream/SysInfoFrame/index.tsx index 21d5916b442..3dfd0927a01 100644 --- a/src/browser/modules/Stream/SysInfoFrame/index.tsx +++ b/src/browser/modules/Stream/SysInfoFrame/index.tsx @@ -21,7 +21,6 @@ import React, { Component } from 'react' import { connect } from 'react-redux' import { withBus } from 'react-suber' -import dateFormat from 'dateformat' import { CYPHER_REQUEST } from 'shared/modules/cypher/cypherDuck' import { isACausalCluster } from 'shared/modules/features/featuresDuck' import { isEnterprise } from 'shared/modules/dbMeta/dbMetaDuck' @@ -156,7 +155,7 @@ export class SysInfoFrame extends Component { {this.state.lastFetch && - `Updated: ${dateFormat(this.state.lastFetch)}`} + `Updated: ${new Date(this.state.lastFetch).toISOString()}`} {this.state.success} svg { + display: inline-block; + vertical-align: middle; +} diff --git a/src/shared/modules/favorites/favoritesDuck.ts b/src/shared/modules/favorites/favoritesDuck.ts index e108e57d05c..48803ff7f88 100644 --- a/src/shared/modules/favorites/favoritesDuck.ts +++ b/src/shared/modules/favorites/favoritesDuck.ts @@ -309,11 +309,11 @@ function updateFavoriteFields( function contentWithNewName(script: Favorite, newName: string): string { // Name of favorite is the comment on the first line by convention - const [nameLine, ...contents] = script.content.split('\n') + const [nameLine, ...NonNamecontents] = script.content.split('\n') const alreadyHasName = nameLine.startsWith('//') return alreadyHasName - ? `// ${newName} - ${contents.join('\n')}` - : `// ${newName} + ? `//${newName} +${NonNamecontents.join('\n')}` + : `//${newName} ${script.content}` } diff --git a/src/shared/modules/sidebar/sidebarDuck.test.ts b/src/shared/modules/sidebar/sidebarDuck.test.ts index d07774803e2..6632851c881 100644 --- a/src/shared/modules/sidebar/sidebarDuck.test.ts +++ b/src/shared/modules/sidebar/sidebarDuck.test.ts @@ -25,13 +25,18 @@ describe('sidebarDuck', () => { const action = toggle('db') const nextState = reducer(undefined, action) - expect(nextState).toEqual({ drawer: 'db', draftScript: null }) + expect(nextState).toEqual({ + drawer: 'db', + draftScript: null, + scriptId: null + }) }) test('should switch drawer when a different one already is open', () => { const initialState: SidebarState = { drawer: 'favorites', - draftScript: null + draftScript: null, + scriptId: null } const action = toggle('db') const nextState = reducer(initialState, action) @@ -41,7 +46,8 @@ describe('sidebarDuck', () => { test('should close drawer when the opened one is toggled', () => { const initialState: SidebarState = { drawer: 'db', - draftScript: null + draftScript: null, + scriptId: null } const action = toggle('db') const nextState = reducer(initialState, action) diff --git a/src/shared/modules/sidebar/sidebarDuck.ts b/src/shared/modules/sidebar/sidebarDuck.ts index 88d6856f6ad..55d0b0e3e4e 100644 --- a/src/shared/modules/sidebar/sidebarDuck.ts +++ b/src/shared/modules/sidebar/sidebarDuck.ts @@ -23,6 +23,7 @@ import { GlobalState } from 'shared/globalState' export const NAME = 'sidebar' export const TOGGLE = 'sidebar/TOGGLE' +export const OPEN = 'sidebar/OPEN' export const SET_DRAFT_SCRIPT = 'sidebar/SET_DRAFT_SCRIPT' export function getOpenDrawer(state: GlobalState): string | null { @@ -33,6 +34,10 @@ export function getCurrentDraft(state: GlobalState): string | null { return state[NAME].draftScript } +export function getScriptDraftId(state: GlobalState): string | null { + return state[NAME].scriptId || null +} + // SIDEBAR type DrawerId = | 'dbms' @@ -47,30 +52,38 @@ type DrawerId = export interface SidebarState { drawer: DrawerId | null draftScript: string | null + scriptId: string | null } const initialState: SidebarState = { drawer: null, - draftScript: null + draftScript: null, + scriptId: null } function toggleDrawer(state: SidebarState, drawer: DrawerId): SidebarState { // When toggling the drawer we clear the script draft if (drawer === state.drawer) { - return { draftScript: null, drawer: null } + return { draftScript: null, drawer: null, scriptId: null } } - return { draftScript: null, drawer } + return { draftScript: null, drawer, scriptId: null } } -type SidebarAction = ToggleAction | SetDraftScriptAction +type SidebarAction = ToggleAction | SetDraftScriptAction | OpenAction interface ToggleAction { type: typeof TOGGLE drawerId: DrawerId } +interface OpenAction { + type: typeof OPEN + drawerId: DrawerId +} + interface SetDraftScriptAction { type: typeof SET_DRAFT_SCRIPT cmd: string | null + scriptId: string | null drawerId: DrawerId } @@ -81,8 +94,14 @@ export default function reducer( switch (action.type) { case TOGGLE: return toggleDrawer(state, action.drawerId) + case OPEN: + return { ...state, drawer: action.drawerId } case SET_DRAFT_SCRIPT: - return { drawer: action.drawerId, draftScript: action.cmd } + return { + drawer: action.drawerId, + scriptId: action.scriptId, + draftScript: action.cmd + } } return state } @@ -91,9 +110,14 @@ export function toggle(drawerId: DrawerId): ToggleAction { return { type: TOGGLE, drawerId } } +export function open(drawerId: DrawerId): OpenAction { + return { type: OPEN, drawerId } +} + export function setDraftScript( cmd: string | null, - drawerId: DrawerId + drawerId: DrawerId, + scriptId: string | null = null ): SetDraftScriptAction { - return { type: SET_DRAFT_SCRIPT, cmd, drawerId } + return { type: SET_DRAFT_SCRIPT, cmd, drawerId, scriptId } } diff --git a/src/shared/services/exporting/favoriteUtils.ts b/src/shared/services/exporting/favoriteUtils.ts index 9f550feccc8..377b191a8b1 100644 --- a/src/shared/services/exporting/favoriteUtils.ts +++ b/src/shared/services/exporting/favoriteUtils.ts @@ -26,6 +26,18 @@ import { Folder } from 'shared/modules/favorites/foldersDuck' export const CYPHER_FILE_EXTENSION = '.cypher' +export function exportFavoritesAsBigCypherFile(favorites: Favorite[]): void { + const fileContent = favorites + .map(favorite => favorite.content) + .join('\n\n') + .trim() + + saveAs( + new Blob([fileContent], { type: 'text/csv' }), + `saved-scripts-${new Date().toISOString().split('T')[0]}.cypher` + ) +} + type WriteableFavorite = { content: string fullFilename: string diff --git a/yarn.lock b/yarn.lock index 2cfaee2f584..51c2c3d60be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1810,6 +1810,21 @@ resolved "https://neo.jfrog.io/neo/api/npm/npm/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@react-dnd/asap@^4.0.0": + version "4.0.0" + resolved "https://neo.jfrog.io/neo/api/npm/npm/@react-dnd/asap/-/asap-4.0.0.tgz#b300eeed83e9801f51bd66b0337c9a6f04548651" + integrity sha1-swDu7YPpgB9RvWawM3yabwRUhlE= + +"@react-dnd/invariant@^2.0.0": + version "2.0.0" + resolved "https://neo.jfrog.io/neo/api/npm/npm/@react-dnd/invariant/-/invariant-2.0.0.tgz#09d2e81cd39e0e767d7da62df9325860f24e517e" + integrity sha1-CdLoHNOeDnZ9faYt+TJYYPJOUX4= + +"@react-dnd/shallowequal@^2.0.0": + version "2.0.0" + resolved "https://neo.jfrog.io/neo/api/npm/npm/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz#a3031eb54129f2c66b2753f8404266ec7bf67f0a" + integrity sha1-owMetUEp8sZrJ1P4QEJm7Hv2fwo= + "@relate-by-ui/css@1.0.5": version "1.0.5" resolved "https://neo.jfrog.io/neo/api/npm/npm/@relate-by-ui/css/-/css-1.0.5.tgz#2c14bc91ebe3c199984d394f9add2f908f9fb695" @@ -2059,11 +2074,6 @@ resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0" integrity sha1-FCZGkqnW4vpNs99eVulLXiVkesA= -"@types/asap@^2.0.0": - version "2.0.0" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/asap/-/asap-2.0.0.tgz#d529e9608c83499a62ae08c871c5e62271aa2963" - integrity sha1-1SnpYIyDSZpirgjIccXmInGqKWM= - "@types/babel__core@^7.1.7": version "7.1.9" resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d" @@ -2157,11 +2167,6 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/invariant@^2.2.30": - version "2.2.34" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/invariant/-/invariant-2.2.34.tgz#05e4f79f465c2007884374d4795452f995720bbe" - integrity sha1-BeT3n0ZcIAeIQ3TUeVRS+ZVyC74= - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.3" resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -2326,7 +2331,15 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.23": +"@types/react@*": + version "17.0.2" + resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8" + integrity sha1-PeJMTv75At2XlaScdfdgy+T3pag= + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/react@^16.9.23": version "16.9.49" resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872" integrity sha1-CdsCHPgImroM2xKkn4AhppzOSHI= @@ -2346,11 +2359,6 @@ resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/semver/-/semver-7.3.4.tgz#43d7168fec6fa0988bb1a513a697b29296721afb" integrity sha1-Q9cWj+xvoJiLsaUTppeykpZyGvs= -"@types/shallowequal@^1.1.1": - version "1.1.1" - resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/shallowequal/-/shallowequal-1.1.1.tgz#aad262bb3f2b1257d94c71d545268d592575c9b1" - integrity sha1-qtJiuz8rElfZTHHVRSaNWSV1ybE= - "@types/sinonjs__fake-timers@^6.0.1": version "6.0.1" resolved "https://neo.jfrog.io/neo/api/npm/npm/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" @@ -3081,11 +3089,6 @@ arrify@^2.0.0, arrify@^2.0.1: resolved "https://neo.jfrog.io/neo/api/npm/npm/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha1-yWVekzHgq81YjSp8rX6ZVvZnAfo= -asap@^2.0.6: - version "2.0.6" - resolved "https://neo.jfrog.io/neo/api/npm/npm/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - ascii-data-table@^2.1.1: version "2.1.1" resolved "https://neo.jfrog.io/neo/api/npm/npm/ascii-data-table/-/ascii-data-table-2.1.1.tgz#8f62f67c0f2625339a7e0b568c2aeed875a410fe" @@ -4681,9 +4684,9 @@ csstype@^2.5.7: integrity sha1-pokwFbkOhN1uhdDjtEKh6E8tvg8= csstype@^3.0.2: - version "3.0.3" - resolved "https://neo.jfrog.io/neo/api/npm/npm/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" - integrity sha1-K0ELvro4upYzNTr/NLBdl1XQZfg= + version "3.0.6" + resolved "https://neo.jfrog.io/neo/api/npm/npm/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" + integrity sha1-hl0LWDPX2NQPTluKbXauo95HJe8= cyclist@^1.0.1: version "1.0.1" @@ -4768,11 +4771,6 @@ date-fns@^1.27.2: resolved "https://neo.jfrog.io/neo/api/npm/npm/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha1-LnG/CxGRU9u0zE6I2epaz7UNwFw= -dateformat@^3.0.3: - version "3.0.3" - resolved "https://neo.jfrog.io/neo/api/npm/npm/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" - integrity sha1-puN0maTZqc+F71hyBE1ikByYia4= - debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://neo.jfrog.io/neo/api/npm/npm/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4975,15 +4973,13 @@ dlv@^1.1.0: resolved "https://neo.jfrog.io/neo/api/npm/npm/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" integrity sha1-XBmKihFFNZbnUUlNSYdLx3MvLnk= -dnd-core@^9.3.2: - version "9.5.1" - resolved "https://neo.jfrog.io/neo/api/npm/npm/dnd-core/-/dnd-core-9.5.1.tgz#e9ec02d33529b68fa528865704d40ac4b14f2baf" - integrity sha1-6ewC0zUpto+lKIZXBNQKxLFPK68= +dnd-core@^11.1.3: + version "11.1.3" + resolved "https://neo.jfrog.io/neo/api/npm/npm/dnd-core/-/dnd-core-11.1.3.tgz#f92099ba7245e49729d2433157031a6267afcc98" + integrity sha1-+SCZunJF5Jcp0kMxVwMaYmevzJg= dependencies: - "@types/asap" "^2.0.0" - "@types/invariant" "^2.2.30" - asap "^2.0.6" - invariant "^2.2.4" + "@react-dnd/asap" "^4.0.0" + "@react-dnd/invariant" "^2.0.0" redux "^4.0.4" dns-equal@^1.0.0: @@ -10875,23 +10871,22 @@ raw-loader@^0.5.1: resolved "https://neo.jfrog.io/neo/api/npm/npm/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" integrity sha1-DD0L6u2KAclm2Xh793goElKpeao= -react-dnd-html5-backend@9.3.2: - version "9.3.2" - resolved "https://neo.jfrog.io/neo/api/npm/npm/react-dnd-html5-backend/-/react-dnd-html5-backend-9.3.2.tgz#4833043aec91e69dd968ac136deebf7535f5eb9d" - integrity sha1-SDMEOuyR5p3ZaKwTbe6/dTX1650= +react-dnd-html5-backend@^11.1.3: + version "11.1.3" + resolved "https://neo.jfrog.io/neo/api/npm/npm/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz#2749f04f416ec230ea193f5c1fbea2de7dffb8f7" + integrity sha1-J0nwT0FuwjDqGT9cH76i3n3/uPc= dependencies: - dnd-core "^9.3.2" + dnd-core "^11.1.3" -react-dnd@9.3.2: - version "9.3.2" - resolved "https://neo.jfrog.io/neo/api/npm/npm/react-dnd/-/react-dnd-9.3.2.tgz#e06c0f566201fbd4617db6d023fcb45629874bbc" - integrity sha1-4GwPVmIB+9RhfbbQI/y0VimHS7w= +react-dnd@^11.1.3: + version "11.1.3" + resolved "https://neo.jfrog.io/neo/api/npm/npm/react-dnd/-/react-dnd-11.1.3.tgz#f9844f5699ccc55dfc81462c2c19f726e670c1af" + integrity sha1-+YRPVpnMxV38gUYsLBn3JuZwwa8= dependencies: + "@react-dnd/shallowequal" "^2.0.0" "@types/hoist-non-react-statics" "^3.3.1" - "@types/shallowequal" "^1.1.1" - dnd-core "^9.3.2" + dnd-core "^11.1.3" hoist-non-react-statics "^3.3.0" - shallowequal "^1.1.0" react-dom@^16.8.6, react-dom@^16.9.0: version "16.13.1"