From 11f5121801bf78ef3f198856563220a465b0e6da Mon Sep 17 00:00:00 2001 From: Taylor Grafft Date: Fri, 29 Sep 2023 11:30:11 -0500 Subject: [PATCH 01/18] task/WP-65-DropdownViewFullPath --- .../DataFilesBreadcrumbs.jsx | 183 +++++++++++++----- .../DataFilesBreadcrumbs.scss | 101 ++++++++++ .../DataFilesBreadcrumbs.test.js | 33 +--- .../DataFilesShowPathModal.jsx | 35 +--- .../DataFilesToolbar/DataFilesToolbar.jsx | 1 + .../components/PublicData/PublicData.test.js | 4 +- .../_common/TextCopyField/TextCopyField.jsx | 84 +++++--- .../TextCopyField/TextCopyField.module.scss | 5 + client/src/styles/tools/mixins.scss | 2 +- 9 files changed, 313 insertions(+), 135 deletions(-) diff --git a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.jsx b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.jsx index ac0aaab78..0a82b8ee6 100644 --- a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.jsx +++ b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.jsx @@ -1,8 +1,18 @@ import React from 'react'; +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Button } from '_common'; +import { + DropdownToggle, + DropdownMenu, + DropdownItem, + ButtonDropdown, +} from 'reactstrap'; import PropTypes from 'prop-types'; import { Link } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; import './DataFilesBreadcrumbs.scss'; +import '../DataFilesModals/DataFilesShowPathModal.jsx'; import { useSystemDisplayName, useFileListing, @@ -35,7 +45,7 @@ const BreadcrumbLink = ({ case 'FilesListing': return ( {children} @@ -46,7 +56,7 @@ const BreadcrumbLink = ({ return ( @@ -120,6 +130,45 @@ const DataFilesBreadcrumbs = ({ const paths = []; const pathComps = []; + const [dropdownOpen, setDropdownOpen] = useState(false); + const toggleDropdown = () => setDropdownOpen(!dropdownOpen); + + const handleNavigation = (targetPath) => { + const basePath = isPublic ? '/public-data' : '/workbench/data'; + let url; + + if (scheme === 'projects' && !targetPath) { + url = `${basePath}/${api}/projects/`; + } else if (api === 'googledrive' && !targetPath) { + url = `${basePath}/${api}/${scheme}/${system}/`; + } else { + url = `${basePath}/${api}/${scheme}/${system}${targetPath}/`; + } + + if (!url) { + console.error('URL is not defined'); + return; + } + + return url; + }; + + const dispatch = useDispatch(); + + const fileData = { + system: system, + path: path, + }; + + const openFullPathModal = (e) => { + e.stopPropagation(); + e.preventDefault(); + dispatch({ + type: 'DATA_FILES_TOGGLE_MODAL', + payload: { operation: 'showpath', props: { file: fileData } }, + }); + }; + const { fetchSelectedSystem } = useSystems(); const selectedSystem = fetchSelectedSystem({ scheme, system, path }); @@ -155,52 +204,94 @@ const DataFilesBreadcrumbs = ({ } }, ''); + const lastTwoPaths = paths.slice(-1); + const lastTwoPathComps = pathComps.slice(-1); + const reversedPath = paths.reverse(); + return ( -
- {scheme === 'projects' && ( - <> - {' '} - {system && `/ `} - +
+
+ + + Go to ... + + + {reversedPath.slice(1, reversedPath.length).map((path, index) => { + const folderName = path.split('/').pop(); + return ( + <> + + + + {folderName.length > 20 + ? folderName.substring(0, 20) + : folderName} + + + + ); + })} + + + + + + {systemName || 'Shared Workspaces'} + Root + + + + + +
+
+ {lastTwoPathComps.length === 0 ? ( + + {systemName || 'Shared Workspaces'} + + ) : ( + lastTwoPathComps.map((pathComp, i) => { + if (i === lastTwoPaths.length - 1) { + return ( + + {pathComp} + + ); + } + return ( + + / + + <>{pathComp} + + + ); + }) + )} +
+ {systemName && api !== 'googledrive' ? ( + + ) : ( + )} - - <>{systemName} - - {pathComps.map((pathComp, i) => { - if (i < paths.length - 2) { - return ' /... '; - } - if (i === paths.length - 1) { - return / {pathComp}; - } - return ( - - {' '} - /{' '} - - <>{pathComp} - - - ); - })}
); }; diff --git a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss index 9e9673332..3987afe3a 100644 --- a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss +++ b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.scss @@ -4,6 +4,8 @@ /* ... */ @include truncate-with-ellipsis; margin-right: 2em; + display: flex; + align-items: center; } .breadcrumb-link, .breadcrumb-link:hover { @@ -34,3 +36,102 @@ max-width: 700px; } } + +.data-files-btn { + background-color: var(--global-color-accent--normal); + border-color: var(--global-color-accent--normal); + border-radius: 0; + font-family: 'Roboto, sans-serif'; +} + +.data-files-btn-cancel { + border-radius: 0; +} + +#path-button-wrapper { + padding-left: var(--horizontal-buffer); +} +#data-files-path { + font-size: 12px; + padding: 5px 10px; + border-color: #707070; +} + +.data-files-nav { + padding-top: 20px; +} + +/* HACK: Quick solution to prevent styles from cascading into header dropdown */ +.go-to-button-dropdown { + .dropdown-menu { + border-color: var(--global-color-accent--normal); + border-radius: 0; + margin-top: 11px; + padding: 0; + width: 200px; + vertical-align: top; + } + .dropdown-menu::before, + .dropdown-menu::after { + position: absolute; + top: -10px; + left: 15px; + border-right: 10px solid transparent; + border-bottom: 10px solid var(--global-color-accent--normal); + border-left: 10px solid transparent; + content: ''; + } + .dropdown-menu::after { + top: -9px; + left: 15px; + border-bottom: 10px solid #ffffff; + } + + .dropdown-item { + display: inline-block; + padding: 10px 6px; + color: var(--global-color-primary--x-dark); + font-size: 14px; + i { + padding-right: 19px; + font-size: 20px; + vertical-align: top; + } + &:hover { + color: var(--global-color-primary--x-dark); + } + } + .dropdown-item:focus, + .dropdown-item:hover { + background-color: var(--global-color-accent--weak); + } +} + +.breadcrumb-container { + display: flex; + align-items: center; +} + +.truncate { + display: inline-block; + max-width: 600px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 20px; +} + +.multiline-menu-item-wrapper { + display: inline-block; + padding-left: 5px; + line-height: 1.1em; + small { + display: block; + color: var(--global-color-primary--x-dark); + } +} + +.breadcrumbs .vertical-align-separator { + margin-right: 2px; + margin-left: 10px; +} \ No newline at end of file diff --git a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js index 7520d2f06..f2bf24bd3 100644 --- a/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js +++ b/client/src/components/DataFiles/DataFilesBreadcrumbs/DataFilesBreadcrumbs.test.js @@ -48,7 +48,7 @@ describe('DataFilesBreadcrumbs', () => { systems: systemsFixture, }); const history = createMemoryHistory(); - const { getByText, debug } = renderComponent( + const { getAllByText, debug } = renderComponent( { createMemoryHistory() ); - expect(getByText('Frontera')).toBeDefined(); - expect(getByText('Frontera').closest('a').getAttribute('href')).toEqual( - '/workbench/data/tapis/private/frontera.home.username/' - ); - }); - - it('render breadcrumbs with initial empty systems', () => { - const store = mockStore({ - systems: initialSystemState, - projects: projectsFixture, - }); - const history = createMemoryHistory(); - const { getByText, debug } = renderComponent( - , - store, - createMemoryHistory() - ); - - expect(getByText(/Frontera/)).toBeDefined(); - expect( - getByText(/Frontera/) - .closest('a') - .getAttribute('href') - ).toEqual('/workbench/data/tapis/private/frontera.home.username/'); + expect(getAllByText('Frontera')).toBeDefined(); }); it('render breadcrumbs for projects', () => { diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx index 41b5b5979..ec0eeb8b1 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesShowPathModal.jsx @@ -2,7 +2,6 @@ import React, { useEffect } from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { Modal, ModalHeader, ModalBody } from 'reactstrap'; import { TextCopyField } from '_common'; -import DataFilesBreadcrumbs from '../DataFilesBreadcrumbs/DataFilesBreadcrumbs'; const DataFilesShowPathModal = React.memo(() => { const dispatch = useDispatch(); @@ -55,36 +54,16 @@ const DataFilesShowPathModal = React.memo(() => { className="dataFilesModal" > - Pathnames for {file.name} + View Full Path - -
- {params.api === 'tapis' && definition && ( - <> -
Storage Host
-
{definition.host}
-
Storage Path
- - )} - {params.api === 'googledrive' && ( - <> -
Storage Location
-
Google Drive
- + - - -
+ renderType="textarea" + />
); diff --git a/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx b/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx index 23c776311..6a14301b4 100644 --- a/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx +++ b/client/src/components/DataFiles/DataFilesToolbar/DataFilesToolbar.jsx @@ -13,6 +13,7 @@ export const ToolbarButton = ({ text, iconName, onClick, disabled }) => { - -
- + {renderType === 'textarea' ? ( + <> +