X-Git-Url: https://git.arvados.org/arvados-workbench2.git/blobdiff_plain/2334faa59461fde7ba9c59abb0b87831866bf301..d8f669aadc5f3d7241395abd6aa764406079d7d3:/src/components/collection-panel-files/collection-panel-files.tsx diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx index 18230f6f..a7001a61 100644 --- a/src/components/collection-panel-files/collection-panel-files.tsx +++ b/src/components/collection-panel-files/collection-panel-files.tsx @@ -7,18 +7,21 @@ import classNames from 'classnames'; import { connect } from 'react-redux'; import { FixedSizeList } from "react-window"; import AutoSizer from "react-virtualized-auto-sizer"; -import { CustomizeTableIcon } from 'components/icon/icon'; +import servicesProvider from 'common/service-provider'; +import { CustomizeTableIcon, DownloadIcon } from 'components/icon/icon'; import { SearchInput } from 'components/search-input/search-input'; -import { ListItemIcon, StyleRulesCallback, Theme, WithStyles, withStyles, Tooltip, IconButton, Checkbox, CircularProgress } from '@material-ui/core'; +import { ListItemIcon, StyleRulesCallback, Theme, WithStyles, withStyles, Tooltip, IconButton, Checkbox, CircularProgress, Button } from '@material-ui/core'; import { FileTreeData } from '../file-tree/file-tree-data'; import { TreeItem, TreeItemStatus } from '../tree/tree'; import { RootState } from 'store/store'; import { WebDAV, WebDAVRequestConfig } from 'common/webdav'; import { AuthState } from 'store/auth/auth-reducer'; import { extractFilesData } from 'services/collection-service/collection-service-files-response'; -import { DefaultIcon, DirectoryIcon, FileIcon } from 'components/icon/icon'; +import { DefaultIcon, DirectoryIcon, FileIcon, BackIcon, SidePanelRightArrowIcon } from 'components/icon/icon'; import { setCollectionFiles } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions'; import { sortBy } from 'lodash'; +import { formatFileSize } from 'common/formatters'; +import { getInlineFileUrl, sanitizeToken } from 'views-components/context-menu/actions/helpers'; export interface CollectionPanelFilesProps { items: any; @@ -39,13 +42,27 @@ export interface CollectionPanelFilesProps { collectionPanel: any; } -type CssRules = "loader" | "wrapper" | "dataWrapper" | "row" | "rowEmpty" | "leftPanel" | "rightPanel" | "pathPanel" | "pathPanelItem" | "rowName" | "listItemIcon" | "rowActive" | "pathPanelMenu" | "rowSelection" | "leftPanelHidden" | "leftPanelVisible" | "searchWrapper" | "searchWrapperHidden"; +type CssRules = "backButton" | "backButtonHidden" | "pathPanelPathWrapper" | "uploadButton" | "uploadIcon" | "loader" | "wrapper" | "dataWrapper" | "row" | "rowEmpty" | "leftPanel" | "rightPanel" | "pathPanel" | "pathPanelItem" | "rowName" | "listItemIcon" | "rowActive" | "pathPanelMenu" | "rowSelection" | "leftPanelHidden" | "leftPanelVisible" | "searchWrapper" | "searchWrapperHidden"; const styles: StyleRulesCallback = (theme: Theme) => ({ wrapper: { display: 'flex', minHeight: '600px', - marginBottom: '1rem' + marginBottom: '1rem', + color: 'rgba(0, 0, 0, 0.87)', + fontSize: '0.875rem', + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + fontWeight: 400, + lineHeight: '1.5', + letterSpacing: '0.01071em' + }, + backButton: { + color: '#00bfa5', + cursor: 'pointer', + float: 'left', + }, + backButtonHidden: { + display: 'none', }, dataWrapper: { minHeight: '500px' @@ -78,8 +95,9 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ justifyContent: 'center' }, searchWrapper: { - width: '100%', - marginBottom: '1rem' + display: 'inline-block', + marginBottom: '1rem', + marginLeft: '1rem', }, searchWrapperHidden: { width: '0px' @@ -102,19 +120,24 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ pathPanel: { padding: '1rem', marginBottom: '1rem', + backgroundColor: '#fff', boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)', }, + pathPanelPathWrapper: { + display: 'inline-block', + }, leftPanel: { flex: 0, padding: '1rem', marginRight: '1rem', whiteSpace: 'nowrap', position: 'relative', - boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)', + backgroundColor: '#fff', + boxShadow: '0px 3px 3px 0px rgb(0 0 0 / 20%), 0px 3px 1px 0px rgb(0 0 0 / 14%), 0px 3px 1px -1px rgb(0 0 0 / 12%)', }, leftPanelVisible: { opacity: 1, - flex: '30%', + flex: '50%', animation: `animateVisible 1000ms ${theme.transitions.easing.easeOut}` }, leftPanelHidden: { @@ -130,17 +153,26 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ }, "100%": { opacity: 1, - flex: '30%', + flex: '50%', } }, rightPanel: { - flex: '70%', + flex: '50%', padding: '1rem', + paddingTop: '2rem', + marginTop: '-1rem', position: 'relative', - boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)', + backgroundColor: '#fff', + boxShadow: '0px 3px 3px 0px rgb(0 0 0 / 20%), 0px 3px 1px 0px rgb(0 0 0 / 14%), 0px 3px 1px -1px rgb(0 0 0 / 12%)', }, pathPanelItem: { cursor: 'pointer', + }, + uploadIcon: { + transform: 'rotate(180deg)' + }, + uploadButton: { + float: 'right', } }); @@ -151,7 +183,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState collectionPanel: state.collectionPanel, collectionPanelFiles: state.collectionPanelFiles, }))((props: CollectionPanelFilesProps & WithStyles & { auth: AuthState }) => { - const { classes, onItemMenuOpen, isWritable, dispatch, collectionPanelFiles, collectionPanel } = props; + const { classes, onItemMenuOpen, onUploadDataClick, isWritable, dispatch, collectionPanelFiles, collectionPanel } = props; const { apiToken, config } = props.auth; const webdavClient = new WebDAV(); @@ -170,14 +202,14 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState const [path, setPath]: any = React.useState([]); const [pathData, setPathData]: any = React.useState({}); const [isLoading, setIsLoading] = React.useState(false); - const [rightClickUsed, setRightClickUsed] = React.useState(false); + const [collectionAutofetchEnabled, setCollectionAutofetchEnabled] = React.useState(false); const [leftSearch, setLeftSearch] = React.useState(''); const [rightSearch, setRightSearch] = React.useState(''); const leftKey = (path.length > 1 ? path.slice(0, path.length - 1) : path).join('/'); const rightKey = path.join('/'); - const leftData = (pathData[leftKey] || []).filter(({ type }) => type === 'directory'); + const leftData = pathData[leftKey] || []; const rightData = pathData[rightKey]; React.useEffect(() => { @@ -187,16 +219,32 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState } }, [props.currentItemUuid]); - const fetchData = (rightKey, ignoreCache = false) => { - const dataExists = !!pathData[rightKey]; - const runningRequest = pathPromise[rightKey]; + const fetchData = (keys, ignoreCache = false) => { + const keyArray = Array.isArray(keys) ? keys : [keys]; - if ((!dataExists || ignoreCache) && !runningRequest) { - setIsLoading(true); + Promise.all(keyArray + .map((key) => { + const dataExists = !!pathData[key]; + const runningRequest = pathPromise[key]; + + if ((!dataExists || ignoreCache) && (!runningRequest || ignoreCache)) { + if (!isLoading) { + setIsLoading(true); + } - webdavClient.propfind(`c=${rightKey}`, webDAVRequestConfig) - .then((request) => { - if (request.responseXML != null) { + pathPromise[key] = true; + + return webdavClient.propfind(`c=${key}`, webDAVRequestConfig); + } + + return Promise.resolve(null); + }) + .filter((promise) => !!promise) + ) + .then((requests) => { + const newState = requests.map((request, index) => { + if (request && request.responseXML != null) { + const key = keyArray[index]; const result: any = extractFilesData(request.responseXML); const sortedResult = sortBy(result, (n) => n.name).sort((n1, n2) => { if (n1.type === 'directory' && n2.type !== 'directory') { @@ -207,67 +255,74 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState } return 0; }); - const newPathData = { ...pathData, [rightKey]: sortedResult }; - setPathData(newPathData); + + return { [key]: sortedResult }; } - }) - .finally(() => { - setIsLoading(false); - delete pathPromise[rightKey]; - }); - - pathPromise[rightKey] = true; - } else { - setTimeout(() => setIsLoading(false), 0); - } + return {}; + }).reduce((prev, next) => { + return { ...next, ...prev }; + }, {}); + + setPathData({ ...pathData, ...newState }); + }) + .finally(() => { + setIsLoading(false); + keyArray.forEach(key => delete pathPromise[key]); + }); }; React.useEffect(() => { if (rightKey) { fetchData(rightKey); + setLeftSearch(''); + setRightSearch(''); } - }, [rightKey]); + }, [rightKey]); // eslint-disable-line react-hooks/exhaustive-deps React.useEffect(() => { const hash = (collectionPanel.item || {}).portableDataHash; - if (hash && rightClickUsed) { - fetchData(rightKey, true); + if (hash && collectionAutofetchEnabled) { + fetchData([leftKey, rightKey], true); } - }, [(collectionPanel.item || {}).portableDataHash]); + }, [(collectionPanel.item || {}).portableDataHash]); // eslint-disable-line react-hooks/exhaustive-deps React.useEffect(() => { if (rightData) { - setCollectionFiles(rightData, false)(dispatch); + const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1); + setCollectionFiles(filtered, false)(dispatch); } - }, [rightData, dispatch]); + }, [rightData, dispatch, rightSearch]); const handleRightClick = React.useCallback( (event) => { event.preventDefault(); - - if (!rightClickUsed) { - setRightClickUsed(true); - } - let elem = event.target; while (elem && elem.dataset && !elem.dataset.item) { elem = elem.parentNode; } - if (!elem) { + if (!elem || !elem.dataset) { return; } const { id } = elem.dataset; - const item: any = { id, data: rightData.find((elem) => elem.id === id) }; + + const item: any = { + id, + data: rightData.find((elem) => elem.id === id), + }; if (id) { onItemMenuOpen(event, item, isWritable); + + if (!collectionAutofetchEnabled) { + setCollectionAutofetchEnabled(true); + } } }, - [onItemMenuOpen, isWritable, rightData] + [onItemMenuOpen, isWritable, rightData] // eslint-disable-line react-hooks/exhaustive-deps ); React.useEffect(() => { @@ -306,7 +361,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState setPath([...path.slice(0, index + 1)]); } - if (parentPath) { + if (parentPath && type === 'directory') { if (path.length > 1) { path.pop() } @@ -317,6 +372,13 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState if (subfolderPath && type === 'directory') { setPath([...path, subfolderPath]); } + + if (elem.dataset.id && type === 'file') { + const item = rightData.find(({id}) => id === elem.dataset.id) || leftData.find(({ id }) => id === elem.dataset.id); + const enhancedItem = servicesProvider.getServices().collectionService.extendFileURL(item); + const fileUrl = sanitizeToken(getInlineFileUrl(enhancedItem.url, config.keepWebServiceUrl, config.keepWebInlineServiceUrl), true); + window.open(fileUrl, '_blank'); + } } if (isCheckbox) { @@ -325,7 +387,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState props.onSelectionToggle(event, item); } }, - [path, setPath, collectionPanelFiles] + [path, setPath, collectionPanelFiles] // eslint-disable-line react-hooks/exhaustive-deps ); const getItemIcon = React.useCallback( @@ -361,33 +423,45 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState (ev, isWritable) => { props.onOptionsMenuOpen(ev, isWritable); }, - [props.onOptionsMenuOpen] + [props.onOptionsMenuOpen] // eslint-disable-line react-hooks/exhaustive-deps ); return ( -
+
- { - path - .map((p: string, index: number) => - {index === 0 ? 'Home' : p} /  - ) - } +
+ { + path + .map((p: string, index: number) => + {index === 0 ? 'Home' : p} /  + ) + } +
onOptionsMenuOpen(ev, isWritable)}> + onClick={(ev) => { + if (!collectionAutofetchEnabled) { + setCollectionAutofetchEnabled(true); + } + onOptionsMenuOpen(ev, isWritable); + }}>
1 ? classes.leftPanelVisible : classes.leftPanelHidden)}> + 1 ? classes.backButton : classes.backButtonHidden}> + setPath([...path.slice(0, path.length -1)])}> + + +
1 ? classes.searchWrapper : classes.searchWrapperHidden}>
@@ -409,11 +483,21 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState const { id, type, name } = filtered[index]; return
{getItemIcon(type, getActiveClass(name))}
{name}
+ key={id}> + {getItemIcon(type, getActiveClass(name))} +
+ {name} +
+ { + getActiveClass(name) ? : null + }
; } } @@ -428,6 +512,24 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
+ { + isWritable && + + }
{ rightData && !isLoading ? @@ -443,7 +545,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState > { ({ index, style }) => { - const { id, type, name } = filtered[index]; + const { id, type, name, size } = filtered[index]; return
{name}
+ + {formatFileSize(size)} +
} }