- {React.Children.map(this.props.children, child =>
- React.cloneElement(child, {
- ...child.props,
- onClick: () => {
- this.setState({ open: false });
- child.props.onClick();
- },
- })
- )}
+ {!isSubmenu && (
+
this.setState({ open: false })}
+ >
+ {this.props.icon && }
+ {this.props.title}
+
+ )}
+
+ {React.Children.map(this.props.children, (child) => {
+ if (React.isValidElement(child) && child.type === BrowserMenu) {
+ return React.cloneElement(child, {
+ ...child.props,
+ parentClose: () => {
+ this.setState({ open: false });
+ this.props.parentClose?.();
+ },
+ });
+ }
+ return child;
+ })}
@@ -61,18 +92,37 @@ export default class BrowserMenu extends React.Component {
if (this.props.disabled) {
classes.push(styles.disabled);
}
- let onClick = null;
+ const entryEvents = {};
if (!this.props.disabled) {
- onClick = () => {
- this.setState({ open: true });
- this.props.setCurrent(null);
- };
+ if (isSubmenu) {
+ entryEvents.onMouseEnter = () => {
+ const rect = this.wrapRef.current.getBoundingClientRect();
+ const width = this.wrapRef.current.clientWidth;
+ const openToLeft = rect.right + width > window.innerWidth;
+ this.setState({ open: true, openToLeft });
+ this.props.setCurrent?.(null);
+ };
+ } else {
+ entryEvents.onClick = () => {
+ this.setState({ open: true, openToLeft: false });
+ this.props.setCurrent(null);
+ };
+ }
}
return (
-
-
+
+ {this.props.icon && }
{this.props.title}
+ {isSubmenu &&
+ React.Children.toArray(this.props.children).some(c => React.isValidElement(c) && c.type === BrowserMenu) && (
+
+ )}
{menu}
@@ -81,12 +131,12 @@ export default class BrowserMenu extends React.Component {
}
BrowserMenu.propTypes = {
- icon: PropTypes.string.isRequired.describe('The name of the icon to place in the menu.'),
+ icon: PropTypes.string.describe('The name of the icon to place in the menu.'),
title: PropTypes.string.isRequired.describe('The title text of the menu.'),
- children: PropTypes.oneOfType([
- PropTypes.arrayOf(PropTypes.node),
- PropTypes.node,
- ]).describe(
+ children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).describe(
'The contents of the menu when open. It should be a set of MenuItem and Separator components.'
),
+ parentClose: PropTypes.func.describe(
+ 'Closes the parent menu when a nested menu item is selected.'
+ ),
};
diff --git a/src/components/BrowserMenu/BrowserMenu.scss b/src/components/BrowserMenu/BrowserMenu.scss
index 9c73ca3341..1cbf889244 100644
--- a/src/components/BrowserMenu/BrowserMenu.scss
+++ b/src/components/BrowserMenu/BrowserMenu.scss
@@ -89,6 +89,38 @@
font-size: 14px;
}
+
+.subMenuBody {
+ position: absolute;
+ top: 0;
+ border-radius: 0 5px 5px 5px;
+ background: #797592;
+ padding: 8px 0;
+ font-size: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
+}
+
+.subMenuBodyLeft {
+ position: absolute;
+ top: 0;
+ border-radius: 5px 0 5px 5px;
+ background: #797592;
+ padding: 8px 0;
+ font-size: 14px;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
+}
+
+.submenuArrow {
+ margin-left: 4px;
+ fill: #66637A;
+}
+
+.entry:hover .submenuArrow {
+ fill: white;
+}
+
.item {
padding: 4px 14px;
white-space: nowrap;
diff --git a/src/components/BrowserMenu/MenuItem.react.js b/src/components/BrowserMenu/MenuItem.react.js
index 5b8e72c51a..84eedf1ad0 100644
--- a/src/components/BrowserMenu/MenuItem.react.js
+++ b/src/components/BrowserMenu/MenuItem.react.js
@@ -19,8 +19,24 @@ const MenuItem = ({ text, disabled, active, greenActive, onClick }) => {
if (greenActive) {
classes.push(styles.greenActive);
}
+
+ const handleClick = (e) => {
+ if (!disabled && onClick) {
+ onClick(e);
+ }
+ };
+
return (
-
+
{text}
);
diff --git a/src/components/Icon/Icon.react.js b/src/components/Icon/Icon.react.js
index 750e80806b..072ae9edf6 100644
--- a/src/components/Icon/Icon.react.js
+++ b/src/components/Icon/Icon.react.js
@@ -7,12 +7,17 @@
*/
import PropTypes from 'lib/PropTypes';
import React from 'react';
+import styles from 'components/Icon/Icon.scss';
-const Icon = ({ name, fill, width, height }) => {
+const Icon = ({ name, fill, width, height, style, className }) => {
const props = {
- width: width,
- height: height,
+ width,
+ height,
+ style,
};
+ if (className) {
+ props.className = styles[className] || className;
+ }
if (fill) {
props.fill = fill;
}
@@ -30,4 +35,5 @@ Icon.propTypes = {
width: PropTypes.number.isRequired.describe('The icon width, in pixels.'),
height: PropTypes.number.isRequired.describe('The icon height, in pixels.'),
fill: PropTypes.string.describe('A valid color, used as the fill property for the SVG.'),
+ style: PropTypes.object.describe('An object containing CSS styles to apply to the icon.'),
};
diff --git a/src/components/Icon/Icon.scss b/src/components/Icon/Icon.scss
new file mode 100644
index 0000000000..0bd22ea857
--- /dev/null
+++ b/src/components/Icon/Icon.scss
@@ -0,0 +1,14 @@
+/*
+ * Copyright (c) 2016-present, Parse, LLC
+ * All rights reserved.
+ *
+ * This source code is licensed under the license found in the LICENSE file in
+ * the root directory of this source tree.
+ */
+@import 'stylesheets/globals.scss';
+
+.menuCheck {
+ margin-right: 6px;
+ margin-bottom: -1px;
+ padding-top: 0;
+}
diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js
index cfd1c51083..84621a84b9 100644
--- a/src/dashboard/Data/Browser/BrowserToolbar.react.js
+++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js
@@ -7,18 +7,18 @@
*/
import BrowserFilter from 'components/BrowserFilter/BrowserFilter.react';
import BrowserMenu from 'components/BrowserMenu/BrowserMenu.react';
-import Icon from 'components/Icon/Icon.react';
import MenuItem from 'components/BrowserMenu/MenuItem.react';
-import prettyNumber from 'lib/prettyNumber';
-import React, { useRef } from 'react';
import Separator from 'components/BrowserMenu/Separator.react';
-import styles from 'dashboard/Data/Browser/Browser.scss';
-import Toolbar from 'components/Toolbar/Toolbar.react';
-import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react';
import ColumnsConfiguration from 'components/ColumnsConfiguration/ColumnsConfiguration.react';
-import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react';
-import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react';
+import Icon from 'components/Icon/Icon.react';
import Toggle from 'components/Toggle/Toggle.react';
+import Toolbar from 'components/Toolbar/Toolbar.react';
+import styles from 'dashboard/Data/Browser/Browser.scss';
+import LoginDialog from 'dashboard/Data/Browser/LoginDialog.react';
+import SecureFieldsDialog from 'dashboard/Data/Browser/SecureFieldsDialog.react';
+import SecurityDialog from 'dashboard/Data/Browser/SecurityDialog.react';
+import prettyNumber from 'lib/prettyNumber';
+import React, { useRef } from 'react';
const BrowserToolbar = ({
className,
@@ -82,6 +82,8 @@ const BrowserToolbar = ({
classwiseCloudFunctions,
appId,
appName,
+ scrollToTop,
+ toggleScrollToTop,
}) => {
const selectionLength = Object.keys(selection).length;
const isPendingEditCloneRows = editCloneRows && editCloneRows.length > 0;
@@ -424,7 +426,7 @@ const BrowserToolbar = ({
)}
{enableSecurityDialog ?
:
}
-
+
)}
+
+
+
+
+
);
};
diff --git a/src/dashboard/Data/Browser/DataBrowser.react.js b/src/dashboard/Data/Browser/DataBrowser.react.js
index 5fda24e448..1d0dbb7d1f 100644
--- a/src/dashboard/Data/Browser/DataBrowser.react.js
+++ b/src/dashboard/Data/Browser/DataBrowser.react.js
@@ -107,6 +107,7 @@ export default class DataBrowser extends React.Component {
showAggregatedData: true,
frozenColumnIndex: -1,
showRowNumber: storedRowNumber,
+ scrollToTop: true,
prefetchCache: {},
selectionHistory: [],
};
@@ -130,8 +131,10 @@ export default class DataBrowser extends React.Component {
this.freezeColumns = this.freezeColumns.bind(this);
this.unfreezeColumns = this.unfreezeColumns.bind(this);
this.setShowRowNumber = this.setShowRowNumber.bind(this);
+ this.toggleScrollToTop = this.toggleScrollToTop.bind(this);
this.handleCellClick = this.handleCellClick.bind(this);
this.saveOrderTimeout = null;
+ this.aggregationPanelRef = React.createRef();
}
componentWillReceiveProps(props) {
@@ -215,6 +218,17 @@ export default class DataBrowser extends React.Component {
this.props.setErrorAggregatedData({});
}
}
+
+ if (
+ (this.props.AggregationPanelData !== prevProps.AggregationPanelData ||
+ this.state.selectedObjectId !== prevState.selectedObjectId) &&
+ this.state.isPanelVisible &&
+ this.aggregationPanelRef?.current
+ ) {
+ if (this.state.scrollToTop) {
+ this.aggregationPanelRef.current.scrollTop = 0;
+ }
+ }
}
handleResizeStart() {
@@ -654,6 +668,10 @@ export default class DataBrowser extends React.Component {
window.localStorage?.setItem(BROWSER_SHOW_ROW_NUMBER, show);
}
+ toggleScrollToTop() {
+ this.setState(prevState => ({ scrollToTop: !prevState.scrollToTop }));
+ }
+
getPrefetchSettings() {
const config =
this.props.classwiseCloudFunctions?.[
@@ -902,7 +920,10 @@ export default class DataBrowser extends React.Component {
resizeHandles={['w']}
className={styles.resizablePanel}
>
-
+
diff --git a/src/dashboard/Data/Views/Views.react.js b/src/dashboard/Data/Views/Views.react.js
index f432d91e11..739976e21e 100644
--- a/src/dashboard/Data/Views/Views.react.js
+++ b/src/dashboard/Data/Views/Views.react.js
@@ -340,7 +340,7 @@ class Views extends TableView {
text = 'Link';
}
content = (
-
+
{text}
);
diff --git a/src/icons/script-solid.svg b/src/icons/script-solid.svg
new file mode 100644
index 0000000000..3941d0bd91
--- /dev/null
+++ b/src/icons/script-solid.svg
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/src/icons/script.svg b/src/icons/script.svg
new file mode 100644
index 0000000000..6be6cf3763
--- /dev/null
+++ b/src/icons/script.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file