diff --git a/frontend/src/metadata/metadata-view/components/context-menu/context-menu.css b/frontend/src/metadata/metadata-view/components/context-menu/context-menu.css new file mode 100644 index 00000000000..0b4aec9148c --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/context-menu/context-menu.css @@ -0,0 +1,39 @@ +.context-menu { + position: fixed; + min-width: 12rem; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 0.875rem; + color: #212529; + text-align: left; + list-style: none; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0, 40, 100, 0.12); + border-radius: 3px; + box-shadow: 0 0 5px #ccc; + z-index: 1000; +} + +.context-menu .context-menu-item { + line-height: 1.5; + cursor: pointer; + min-height: 28px; + font-weight: 400; + color: #373a3c; + text-align: inherit; + white-space: nowrap; + background: 0 0; + border: 0; + display: block; + width: 100%; + padding: 0.25rem 1.5rem; + clear: both; +} + +.context-menu .context-menu-item:hover { + color: #fff; + background-color: #20a0ff; + border-color: #20a0ff; + text-decoration: none; +} diff --git a/frontend/src/metadata/metadata-view/components/context-menu/context-menu.jsx b/frontend/src/metadata/metadata-view/components/context-menu/context-menu.jsx new file mode 100644 index 00000000000..ed4f9522444 --- /dev/null +++ b/frontend/src/metadata/metadata-view/components/context-menu/context-menu.jsx @@ -0,0 +1,63 @@ +import React, { useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import './context-menu.css'; + +const ContextMenu = ({ position, options, onOptionClick, visible, onCloseContextMenu }) => { + const menuRef = useRef(null); + + useEffect(() => { + const handleCloseContextMenu = (event) => { + if (menuRef.current && !menuRef.current.contains(event.target)) { + onCloseContextMenu(); + } + }; + + if (visible) { + document.addEventListener('mousedown', handleCloseContextMenu); + } + + return () => { + document.removeEventListener('mousedown', handleCloseContextMenu); + }; + }, [visible, onCloseContextMenu]); + + if (!visible) return null; + + return ( + + ); +}; + +ContextMenu.propTypes = { + position: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }).isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }) + ).isRequired, + onOptionClick: PropTypes.func.isRequired, + visible: PropTypes.bool.isRequired, +}; + +export default ContextMenu; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/body.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/body.js index 13e28159678..c468d6976cd 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/body.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/body.js @@ -409,7 +409,7 @@ class RecordsBody extends Component { onCellMouseMove: this.onCellMouseMove, onDragEnter: this.handleDragEnter, modifyRecord: this.props.modifyRecord, - onContextMenu: this.props.onFileNameContextMenu + onContextMenu: this.props.onContextMenu }; return this.cellMetaData; }; @@ -601,7 +601,7 @@ RecordsBody.propTypes = { openDownloadFilesDialog: PropTypes.func, cacheDownloadFilesProps: PropTypes.func, onRowExpand: PropTypes.func, - onFileNameContextMenu: PropTypes.func, + onContextMenu: PropTypes.func, }; export default RecordsBody; diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/index.js index 9962147d6c0..ea1a8e3be19 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/index.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/index.js @@ -15,13 +15,10 @@ import RecordMetrics from '../../../../utils/record-metrics'; import { isShiftKeyDown } from '../../../../utils/keyboard-utils'; import { getVisibleBoundaries } from '../../../../utils/viewport'; import { getColOverScanEndIdx, getColOverScanStartIdx } from '../../../../utils/grid'; -import { hideMenu, showMenu } from '../../../../../../components/context-menu/actions'; -import ContextMenu from '../../../../../../components/context-menu/context-menu'; import TextTranslation from '../../../../../../utils/text-translation'; import { Utils } from '../../../../../../utils/utils'; import { siteRoot } from '../../../../../../utils/constants'; - -const METADATA_RECORD_CONTEXT_MENU = 'metadata-record-context-menu'; +import ContextMenu from '../../../context-menu/context-menu'; class Records extends Component { @@ -46,11 +43,17 @@ class Records extends Component { bottomRight: this.initPosition, }, selectedPosition: this.initPosition, + contextMenuPosition: { x: 0, y: 0 }, + isContextMenuVisible: false, ...initHorizontalScrollState, }; this.isWindows = isWindowsBrowser(); this.isWebkit = isWebkitBrowser(); this.baseURI = ''; + this.contextMenuOptions = [ + { label: TextTranslation.OPEN_FILE_IN_NEW_TAB.value, value: 'openFileInNewTab' }, + { label: TextTranslation.OPEN_PARENT_FOLDER.value, value: 'openParentFolder' }, + ]; } componentDidMount() { @@ -603,18 +606,14 @@ class Records extends Component { const record = this.props.recordGetter(rowIdx); const repoID = window.sfMetadataStore.repoId; + let url; if (record._is_dir) { - let url; - if (record._parent_dir === '/') { - url = this.baseURI + record._parent_dir + record._name; - } else { - url = this.baseURI + record._parent_dir + '/' + record._name; - } - window.open(url, '_blank'); + url = `${this.baseURI}${record._parent_dir === '/' ? '' : record._parent_dir + '/'}${record._name}`; } else { - const url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(record._parent_dir + '/' + record._name); - window.open(url, '_blank'); + url = `${siteRoot}lib/${repoID}/file${Utils.encodePath(record._parent_dir + '/' + record._name)}`; } + + window.open(url, '_blank'); }; onOpenParentFolder = (event) => { @@ -628,73 +627,46 @@ class Records extends Component { location.href = url; }; - onMenuItemClick = (operation, obj, event) => { - hideMenu(); - switch (operation) { - case 'Open file in new tab': - this.onOpenInNewTab(event); - return; - case 'Open parent folder': - this.onOpenParentFolder(event); - return; - default: - return; - } - }; - - getMenuList = () => { - const { OPEN_FILE_IN_NEW_TAB, OPEN_PARENT_FOLDER } = TextTranslation; - return [OPEN_FILE_IN_NEW_TAB, OPEN_PARENT_FOLDER]; - }; - - handleContextMenu = (event, id, menuList, currentObject = null) => { - event.preventDefault(); - event.stopPropagation(); - - let x = event.clientX || (event.touches && event.touches[0].pageX); - let y = event.clientY || (event.touches && event.touches[0].pageY); - - if (this.props.posX) { - x -= this.props.posX; - } - if (this.props.posY) { - y -= this.props.posY; - } - - hideMenu(); + onOptionClick = (event, option) => { + this.setState({ + isContextMenuVisible: false, + }); - this.setState({ activeDirent: currentObject }); + const handlers = { + openFileInNewTab: this.onOpenInNewTab.bind(this), + openParentFolder: this.onOpenParentFolder.bind(this), + }; - if (menuList.length === 0) { - return; + const handler = handlers[option.value]; + if (handler) { + handler(event); } + }; - showMenu({ - id: id, - position: { x, y }, - target: event.target, - currentObject: currentObject, - menuList: menuList, - }); + onCloseContextMenu = () => { + this.setState({ isContextMenuVisible: false }); }; - onFileNameContextMenu = (event, cell) => { + onContextMenu = (event, cell) => { const record = this.props.recordGetter(cell.rowIdx); if (record._is_dir) { return; } - this.baseURI = event.target.baseURI; - this.setState({ selectedPosition: cell }); - this.handleContextMenu(event, METADATA_RECORD_CONTEXT_MENU, this.getMenuList()); - }; + const { clientX, clientY, touches, target } = event; - getMenuContainerSize = () => { - if (this.resultContainerRef) { - const { offsetWidth: width, offsetHeight: height } = this.resultContainerRef; - return { width, height }; - } - return { width: 0, height: 0 }; + // Calculate x and y coordinates + const x = (clientX || touches?.[0]?.pageX) - this.resultContainerRef.getBoundingClientRect().left - (this.props.posX || 0); + const y = (clientY || touches?.[0]?.pageY) - this.resultContainerRef.getBoundingClientRect().top - (this.props.posY || 0); + + const position = { x, y }; + + this.baseURI = target.baseURI; + this.setState({ + selectedPosition: cell, + isContextMenuVisible: true, + contextMenuPosition: position + }); }; renderRecordsBody = ({ containerWidth }) => { @@ -716,7 +688,7 @@ class Records extends Component { setRecordsScrollLeft: this.setScrollLeft, hasSelectedCell: this.hasSelectedCell, cacheScrollTop: this.storeScrollTop, - onFileNameContextMenu: this.onFileNameContextMenu, + onContextMenu: this.onContextMenu, }; if (this.props.isGroupView) { return ( @@ -779,9 +751,11 @@ class Records extends Component { {this.renderRecordsBody({ containerWidth })} {this.isWindows && this.isWebkit && ( diff --git a/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.js b/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.js index 688826cdfd4..88ca9e5c488 100644 --- a/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.js +++ b/frontend/src/metadata/metadata-view/components/table/table-main/records/record/cell/index.js @@ -103,6 +103,8 @@ const Cell = React.memo(({ }, []); const onContextMenu = useCallback((event) => { + event.preventDefault(); + event.stopPropagation(); if (column.idx !== 0) return; const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };