diff --git a/package-lock.json b/package-lock.json index 0a70b18699f..b5eed50678e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63423,6 +63423,7 @@ "@react-stately/tooltip": "^3.0.5", "@types/lodash": "^4.14.172", "bson": "^4.6.1", + "focus-trap-react": "^8.4.2", "hadron-document": "^7.13.0", "hadron-type-checker": "^6.10.0", "lodash": "^4.17.21", @@ -77890,7 +77891,6 @@ "@testing-library/user-event": "^13.5.0", "ace-builds": "^1.4.3", "chai": "^4.2.0", - "classnames": "^2.2.6", "debug": "^4.2.0", "depcheck": "^1.4.1", "electron": "^13.5.1", @@ -77905,9 +77905,7 @@ "nyc": "^15.1.0", "object-diff": "0.0.4", "prop-types": "^15.7.2", - "react-bootstrap": "^0.32.1", "react-dom": "^16.14.0", - "react-fontawesome": "^1.6.1", "reflux": "^0.4.1", "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "rimraf": "^3.0.1", @@ -103779,6 +103777,7 @@ "bson": "^4.6.1", "chai": "^4.3.4", "eslint": "^7.25.0", + "focus-trap-react": "^8.4.2", "hadron-document": "^7.13.0", "hadron-type-checker": "^6.10.0", "lodash": "^4.17.21", @@ -112997,7 +112996,6 @@ "@testing-library/user-event": "^13.5.0", "ace-builds": "^1.4.3", "chai": "^4.2.0", - "classnames": "^2.2.6", "debug": "^4.2.0", "depcheck": "^1.4.1", "electron": "^13.5.1", @@ -113015,9 +113013,7 @@ "object-diff": "0.0.4", "prop-types": "^15.7.2", "react": "^16.14.0", - "react-bootstrap": "^0.32.1", "react-dom": "^16.14.0", - "react-fontawesome": "^1.6.1", "reflux": "^0.4.1", "reflux-state-mixin": "github:mongodb-js/reflux-state-mixin", "rimraf": "^3.0.1", diff --git a/packages/compass-collection/src/stores/context.tsx b/packages/compass-collection/src/stores/context.tsx index f0cb8c58fee..002ad4e64b3 100644 --- a/packages/compass-collection/src/stores/context.tsx +++ b/packages/compass-collection/src/stores/context.tsx @@ -79,6 +79,11 @@ type ContextProps = { connectionString?: string; }; +type ContextWithAppRegistry = ContextProps & { + globalAppRegistry: AppRegistry; + localAppRegistry: AppRegistry; +}; + /** * Setup a scoped store to the collection. * @@ -115,7 +120,7 @@ const setupStore = ({ query, aggregation, connectionString, -}: ContextProps) => { +}: ContextWithAppRegistry) => { const store = role.configureStore({ localAppRegistry, globalAppRegistry, @@ -138,7 +143,7 @@ const setupStore = ({ aggregation, connectionString, }); - localAppRegistry?.registerStore(role.storeName, store); + localAppRegistry.registerStore(role.storeName, store); return store; }; @@ -176,7 +181,7 @@ const setupPlugin = ({ sourceName, connectionString, key, -}: ContextProps) => { +}: ContextWithAppRegistry) => { const actions = role.configureActions(); const store = setupStore({ role, @@ -232,7 +237,7 @@ const setupScopedModals = ({ isFLE, sourceName, connectionString, -}: ContextProps) => { +}: ContextWithAppRegistry) => { const roles = globalAppRegistry?.getRole('Collection.ScopedModal'); if (roles) { return roles.map((role: any, i: number) => { @@ -257,6 +262,71 @@ const setupScopedModals = ({ return []; }; +/** + * Setup the query bar plugins. Need to instantiate the store and actions + * and put them in the app registry for use by all the plugins. This way + * there is only 1 query bar store per collection tab instead of one per + * plugin that uses it. + */ +const setupQueryPlugins = ({ + globalAppRegistry, + localAppRegistry, + serverVersion, + state, + namespace, + isReadonly, + isTimeSeries, + isClustered, + isFLE, + query, + aggregation, +}: ContextWithAppRegistry): void => { + const queryBarRole = globalAppRegistry.getRole('Query.QueryBar')?.[0]; + if (queryBarRole) { + localAppRegistry.registerRole('Query.QueryBar', queryBarRole); + const queryBarActions = setupActions(queryBarRole, localAppRegistry); + setupStore({ + role: queryBarRole, + globalAppRegistry, + localAppRegistry, + dataService: state.dataService, + namespace, + serverVersion, + isReadonly, + isTimeSeries, + isClustered, + isFLE, + actions: queryBarActions, + query, + aggregation, + }); + } + + const queryHistoryRole = globalAppRegistry.getRole('Query.QueryHistory')?.[0]; + if (queryHistoryRole) { + localAppRegistry.registerRole('Query.QueryHistory', queryHistoryRole); + const queryHistoryActions = setupActions( + queryHistoryRole, + localAppRegistry + ); + setupStore({ + role: queryHistoryRole, + globalAppRegistry, + localAppRegistry, + dataService: state.dataService, + namespace, + serverVersion, + isReadonly, + isTimeSeries, + isClustered, + isFLE, + actions: queryHistoryActions, + query, + aggregation, + }); + } +}; + /** * Create the context in which a tab is created. * @@ -306,25 +376,16 @@ const createContext = ({ const views: JSX.Element[] = []; const queryHistoryIndexes: number[] = []; - // Setup the query bar plugin. Need to instantiate the store and actions - // and put them in the app registry for use by all the plugins. This way - // there is only 1 query bar store per collection tab instead of one per - // plugin that uses it. - const queryBarRole = globalAppRegistry.getRole('Query.QueryBar')[0]; - localAppRegistry.registerRole('Query.QueryBar', queryBarRole); - const queryBarActions = setupActions(queryBarRole, localAppRegistry); - setupStore({ - role: queryBarRole, + setupQueryPlugins({ globalAppRegistry, localAppRegistry, - dataService: state.dataService, - namespace, serverVersion, + state, + namespace, isReadonly, isTimeSeries, isClustered, isFLE, - actions: queryBarActions, query, aggregation, }); diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json index 6a7b62a5509..19f32c8360b 100644 --- a/packages/compass-components/package.json +++ b/packages/compass-components/package.json @@ -73,6 +73,7 @@ "@react-stately/tooltip": "^3.0.5", "@types/lodash": "^4.14.172", "bson": "^4.6.1", + "focus-trap-react": "^8.4.2", "hadron-document": "^7.13.0", "hadron-type-checker": "^6.10.0", "lodash": "^4.17.21", diff --git a/packages/compass-components/src/components/interactive-popover.tsx b/packages/compass-components/src/components/interactive-popover.tsx new file mode 100644 index 00000000000..d7e455b2d55 --- /dev/null +++ b/packages/compass-components/src/components/interactive-popover.tsx @@ -0,0 +1,141 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { css } from '@leafygreen-ui/emotion'; +import FocusTrap from 'focus-trap-react'; + +import { Popover } from './leafygreen'; + +const contentContainerStyles = css({ + display: 'flex', + height: '100%', +}); + +type InteractivePopoverProps = { + className: string; + children: (childrenProps: { onClose: () => void }) => React.ReactElement; + trigger: (triggerProps: { + onClick: (event: React.MouseEvent | React.TouchEvent) => void; + ref: React.RefObject; + children: React.ReactNode; + }) => React.ReactElement; + open: boolean; + setOpen: (open: boolean) => void; +}; + +function InteractivePopover({ + className, + children, + trigger, + open, + setOpen, +}: InteractivePopoverProps): React.ReactElement { + const triggerRef = useRef(null); + const popoverContentContainerRef = useRef(null); + + const onClose = useCallback(() => { + setOpen(false); + + // Return focus to the trigger when the popover is hidden. + setTimeout(() => { + triggerRef.current?.focus(); + }); + }, [setOpen]); + + const onClickTrigger = useCallback(() => { + if (open) { + onClose(); + return; + } + + setOpen(!open); + }, [open, setOpen, onClose]); + + // When the popover is open, close it when an item that isn't the popover + // is clicked. + useEffect(() => { + if (!open) { + return; + } + + const clickEventListener = (event: MouseEvent | TouchEvent) => { + // Ignore clicks on the popover. + if ( + !popoverContentContainerRef.current || + popoverContentContainerRef.current.contains(event.target as Node) + ) { + return; + } + // Ignore clicks on the trigger as it has its own handler. + if ( + !triggerRef.current || + triggerRef.current.contains(event.target as Node) + ) { + return; + } + + onClose(); + }; + window.addEventListener('mousedown', clickEventListener); + window.addEventListener('touchstart', clickEventListener); + return () => { + window.removeEventListener('mousedown', clickEventListener); + window.removeEventListener('touchstart', clickEventListener); + }; + }, [open, onClose]); + + const onPopoverKeyDown = useCallback( + (evt: KeyboardEvent) => { + if (evt.key === 'Escape') { + onClose(); + return; + } + }, + [onClose] + ); + + useEffect(() => { + if (!open) { + return; + } + document.addEventListener('keydown', onPopoverKeyDown); + + return () => { + document.removeEventListener('keydown', onPopoverKeyDown); + }; + }, [onPopoverKeyDown, open]); + + return trigger({ + onClick: onClickTrigger, + ref: triggerRef, + children: ( + + {open && ( + +
+ {children({ + onClose: onClose, + })} +
+
+ )} +
+ ), + }); +} + +export { InteractivePopover }; diff --git a/packages/compass-components/src/components/leafygreen.tsx b/packages/compass-components/src/components/leafygreen.tsx index 2bb25517b0b..d1a21a5543a 100644 --- a/packages/compass-components/src/components/leafygreen.tsx +++ b/packages/compass-components/src/components/leafygreen.tsx @@ -25,6 +25,7 @@ import { default as LeafyGreenModal, Footer as LeafyGreenModalFooter, } from '@leafygreen-ui/modal'; +import Popover from '@leafygreen-ui/popover'; import { RadioBox, RadioBoxGroup } from '@leafygreen-ui/radio-box-group'; import { Radio, @@ -160,6 +161,7 @@ export { ModalFooter, MongoDBLogoMark, MongoDBLogo, + Popover, RadioBox, RadioBoxGroup, Radio, diff --git a/packages/compass-components/src/components/toolbar.tsx b/packages/compass-components/src/components/toolbar.tsx index 711d9c07a0c..2dc48d72cd2 100644 --- a/packages/compass-components/src/components/toolbar.tsx +++ b/packages/compass-components/src/components/toolbar.tsx @@ -3,10 +3,9 @@ import { cx, css } from '@leafygreen-ui/emotion'; import { uiColors } from '@leafygreen-ui/palette'; import { withTheme } from '../hooks/use-theme'; -import { gray8 } from '../compass-ui-colors'; const toolbarLightThemeStyles = css({ - backgroundColor: gray8, + backgroundColor: uiColors.white, color: uiColors.gray.dark2, }); diff --git a/packages/compass-components/src/constants/mongodb-ace-theme-query.ts b/packages/compass-components/src/constants/mongodb-ace-theme-query.ts index f3f0b9589f0..6434a8ad162 100644 --- a/packages/compass-components/src/constants/mongodb-ace-theme-query.ts +++ b/packages/compass-components/src/constants/mongodb-ace-theme-query.ts @@ -1,116 +1,5 @@ -const mongodbAceThemeQueryCssTextLegacy = ` -.ace-mongodb-query .ace_gutter { -background: #ffffff; -color: #999999; -} -.ace-mongodb-query { -background: #ffffff; -color: #000; -} -.ace-mongodb-query .ace_placeholder { -font-family: inherit; -transform: none; -opacity: 1; -margin: 0; -} -.ace-mongodb-query .ace_keyword { -color: #999999; -font-weight: normal; -} -.ace-mongodb-query .ace_gutter-cell { -padding-left: 5px; -padding-right: 10px; -} -.ace-mongodb-query .ace_string { -color: #5b81a9; -} -.ace-mongodb-query .ace_boolean { -color: #5b81a9; -font-weight: normal; -} -.ace-mongodb-query .ace_constant.ace_numeric { -color: #5b81a9; -} -.ace-mongodb-query .ace_string.ace_regexp { -color: #5b81a9; -} -.ace-mongodb-query .ace_variable.ace_class { -color: teal; -} -.ace-mongodb-query .ace_constant.ace_buildin { -color: #0086B3; -} -.ace-mongodb-query .ace_support.ace_function { -color: #0086B3; -} -.ace-mongodb-query .ace_comment { -color: #998; -font-style: italic; -} -.ace-mongodb-query .ace_variable.ace_language { -color: #0086B3; -} -.ace-mongodb-query .ace_paren { -font-weight: normal; -} -.ace-mongodb-query .ace_variable.ace_instance { -color: teal; -} -.ace-mongodb-query .ace_constant.ace_language { -font-weight: bold; -} -.ace-mongodb-query .ace_cursor { -color: #999999; -} -.ace-mongodb-query.ace_focus .ace_marker-layer .ace_active-line { -background: #ffffff; -} -.ace-mongodb-query .ace_marker-layer .ace_active-line { -background: #ffffff; -} -.ace-mongodb-query .ace_marker-layer .ace_selection { -background: rgb(181, 213, 255); -} -.ace-mongodb-query.ace_multiselect .ace_selection.ace_start { -box-shadow: 0 0 3px 0px white; -} -.ace-mongodb-query.ace_nobold .ace_line > span { -font-weight: normal !important; -} -.ace-mongodb-query .ace_marker-layer .ace_step { -background: rgb(252, 255, 0); -} -.ace-mongodb-query .ace_marker-layer .ace_stack { -background: rgb(164, 229, 101); -} -.ace-mongodb-query .ace_marker-layer .ace_bracket { -margin: -1px 0 0 -1px; -border: 1px solid rgb(192, 192, 192); -} -.ace-mongodb-query .ace_gutter-active-line { -background: #ffffff; -} -.ace-mongodb-query .ace_marker-layer .ace_selected-word { -background: rgb(250, 250, 255); -border: 1px solid rgb(200, 200, 250); -} -.ace-mongodb-query .ace_invisible { -color: #BFBFBF -} -.ace-mongodb-query .ace_print-margin { -width: 1px; -background: #e8e8e8; -} -.ace-mongodb-query .ace_hidden-cursors { - opacity: 0; -} -.ace-mongodb-query .ace_indent-guide { -background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y; -}`; - const mongodbAceThemeQueryCssText = ` .ace-mongodb-query .ace_scroller { -margin: 0px 6px; line-height: 14px; } .ace-mongodb-query .ace_gutter { @@ -126,7 +15,7 @@ font-family: inherit; transform: none; opacity: 1; margin: 0; -padding: 6px !important; +padding: 6px 14px !important; } .ace-mongodb-query .ace_keyword { color: #999999; @@ -225,10 +114,7 @@ background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZg function mongodbAceThemeQuery(acequire: any, exports: any) { exports.isDark = false; exports.cssClass = 'ace-mongodb-query'; - exports.cssText = - process?.env?.COMPASS_SHOW_NEW_TOOLBARS === 'true' - ? mongodbAceThemeQueryCssText - : mongodbAceThemeQueryCssTextLegacy; + exports.cssText = mongodbAceThemeQueryCssText; const dom = acequire('../lib/dom'); dom.importCssString(exports.cssText, exports.cssClass); } diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index a86714a6c00..de0f64f8e5c 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -104,6 +104,7 @@ export { ErrorBoundary } from './components/error-boundary'; export { TabNavBar } from './components/tab-nav-bar'; export { WorkspaceContainer } from './components/workspace-container'; export { InlineInfoLink } from './components/inline-info-link'; +export { InteractivePopover } from './components/interactive-popover'; export { Placeholder } from './components/placeholder'; export { useDOMRect } from './hooks/use-dom-rect'; export { VirtualGrid } from './components/virtual-grid'; diff --git a/packages/compass-crud/src/components/add-data-menu.tsx b/packages/compass-crud/src/components/add-data-menu.tsx index c9ac32f0159..b81dee9222e 100644 --- a/packages/compass-crud/src/components/add-data-menu.tsx +++ b/packages/compass-crud/src/components/add-data-menu.tsx @@ -17,6 +17,10 @@ const tooltipContainerStyles = css({ alignItems: 'center', }); +const addDataButtonStyles = css({ + whiteSpace: 'nowrap', +}); + type AddDataMenuProps = { instanceDescription: string; insertDataHandler: (openInsertKey: 'insert-document' | 'import-file') => void; @@ -33,6 +37,7 @@ function AddDataButton({ return ( - ); - } -} - -export default OptionsToggle; -export { OptionsToggle }; diff --git a/packages/compass-query-bar/src/components/legacy-options-toggle/options-toggle.module.less b/packages/compass-query-bar/src/components/legacy-options-toggle/options-toggle.module.less deleted file mode 100644 index 2b0d0c06ad5..00000000000 --- a/packages/compass-query-bar/src/components/legacy-options-toggle/options-toggle.module.less +++ /dev/null @@ -1,15 +0,0 @@ -.component { - margin-right: 5px; - margin-top: 3px; - padding-left: 5px !important; - cursor: pointer; - border-radius: 0 9px 9px 0 !important; - z-index: 100; - display: flex; - flex-direction: row; - align-items: center; - - &.is-open { - border-bottom-right-radius: 0 !important; - } -} diff --git a/packages/compass-query-bar/src/components/legacy-options-toggle/options-toggle.spec.jsx b/packages/compass-query-bar/src/components/legacy-options-toggle/options-toggle.spec.jsx deleted file mode 100644 index 6e55f5a8273..00000000000 --- a/packages/compass-query-bar/src/components/legacy-options-toggle/options-toggle.spec.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { expect } from 'chai'; -import sinon from 'sinon'; -import FontAwesome from 'react-fontawesome'; - -import OptionsToggle from '.'; - -describe('OptionsToggle [Component]', function () { - let actions; - - beforeEach(function (done) { - actions = { toggleQueryOptions: sinon.stub() }; - done(); - }); - - afterEach(function (done) { - actions = null; - done(); - }); - - describe('#rendering', function () { - it('should render the correct icon when it is not expanded', function () { - const component = shallow( - - ); - expect(component.find(FontAwesome)).to.have.prop('name', 'caret-right'); - }); - - it('should render the correct icon when it is expanded', function () { - const component = shallow(); - expect(component.find(FontAwesome)).to.have.prop('name', 'caret-down'); - }); - - it('should render the correct text', function () { - const component = shallow( - - ); - expect( - component.find('[data-test-id="query-bar-options-toggle-text"]') - ).to.have.text('Options'); - }); - }); - - describe('#behaviour', function () { - it('should trigger the toggleQueryOptions action when clicked', function () { - const component = shallow( - - ); - - component.simulate('click'); - expect(actions.toggleQueryOptions).to.be.calledOnce; - }); - }); -}); diff --git a/packages/compass-query-bar/src/components/legacy-query-bar/index.js b/packages/compass-query-bar/src/components/legacy-query-bar/index.js deleted file mode 100644 index 9f66da8b348..00000000000 --- a/packages/compass-query-bar/src/components/legacy-query-bar/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import QueryBar from './query-bar'; - -export default QueryBar; -export { QueryBar }; diff --git a/packages/compass-query-bar/src/components/legacy-query-bar/query-bar.jsx b/packages/compass-query-bar/src/components/legacy-query-bar/query-bar.jsx deleted file mode 100644 index 6f274309d0d..00000000000 --- a/packages/compass-query-bar/src/components/legacy-query-bar/query-bar.jsx +++ /dev/null @@ -1,459 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import classnames from 'classnames'; -import { Dropdown, MenuItem } from 'react-bootstrap'; -import { pick, isEqual, isString, isArray, map } from 'lodash'; -import FontAwesome from 'react-fontawesome'; - -import QueryOption from '../legacy-query-option'; -import OptionsToggle from '../legacy-options-toggle'; -import QUERY_PROPERTIES from '../../constants/query-properties'; - -import styles from './query-bar.module.less'; - -/** - * @type {Record} - */ -const OPTION_DEFINITION = { - filter: { - type: 'document', - placeholder: "{ field: 'value' }", - link: 'https://docs.mongodb.com/compass/current/query/filter/', - }, - project: { - type: 'document', - placeholder: '{ field: 0 }', - link: 'https://docs.mongodb.com/manual/tutorial/project-fields-from-query-results/', - }, - sort: { - type: 'document', - placeholder: "{ field: -1 } or [['field', -1]]", - link: 'https://docs.mongodb.com/manual/reference/method/cursor.sort/', - }, - collation: { - type: 'document', - placeholder: "{ locale: 'simple' }", - link: 'https://docs.mongodb.com/master/reference/collation/', - }, - skip: { - type: 'numeric', - placeholder: '0', - link: 'https://docs.mongodb.com/manual/reference/method/cursor.skip/', - }, - limit: { - type: 'numeric', - placeholder: '0', - link: 'https://docs.mongodb.com/manual/reference/method/cursor.limit/', - }, - maxTimeMS: { - label: 'Max Time MS', - type: 'numeric', - placeholder: '60000', - link: 'https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS/', - }, - sample: { - type: 'boolean', - placeholder: null, - link: 'https://docs.mongodb.com/TBD', - }, -}; - -class QueryBar extends Component { - static displayName = 'QueryBar'; - - static propTypes = { - filter: PropTypes.object, - project: PropTypes.object, - sort: PropTypes.object, - collation: PropTypes.object, - skip: PropTypes.number, - limit: PropTypes.number, - maxTimeMS: PropTypes.number, - sample: PropTypes.bool, - - valid: PropTypes.bool, - filterValid: PropTypes.bool, - projectValid: PropTypes.bool, - collationValid: PropTypes.bool, - sortValid: PropTypes.bool, - skipValid: PropTypes.bool, - limitValid: PropTypes.bool, - - autoPopulated: PropTypes.bool, - filterString: PropTypes.string, - projectString: PropTypes.string, - collationString: PropTypes.string, - sortString: PropTypes.string, - skipString: PropTypes.string, - limitString: PropTypes.string, - - filterPlaceholder: PropTypes.string, - projectPlaceholder: PropTypes.string, - collationPlaceholder: PropTypes.string, - sortPlaceholder: PropTypes.string, - skipPlaceholder: PropTypes.string, - limitPlaceholder: PropTypes.string, - maxTimeMSPlaceholder: PropTypes.string, - - actions: PropTypes.object, - buttonLabel: PropTypes.string, - queryState: PropTypes.string, - serverVersion: PropTypes.string, - layout: PropTypes.array, - expanded: PropTypes.bool, - lastExecutedQuery: PropTypes.object, - onReset: PropTypes.func, - onApply: PropTypes.func, - resultId: PropTypes.number, - schemaFields: PropTypes.array, - showQueryHistoryButton: PropTypes.bool, - showExportToLanguageButton: PropTypes.bool, - }; - - static defaultProps = { - expanded: false, - buttonLabel: 'Apply', - layout: [ - 'filter', - 'project', - ['sort', 'maxTimeMS'], - ['collation', 'skip', 'limit'], - ], - schemaFields: [], - showQueryHistoryButton: true, - showExportToLanguageButton: true, - resultId: 0, - }; - - state = { - hasFocus: false, - }; - - onChange(label, evt) { - const type = OPTION_DEFINITION[label].type; - const { actions } = this.props; - - if (['numeric', 'document'].includes(type)) { - return actions.typeQueryString(label, evt.target.value); - } - if (type === 'boolean') { - // there is only one boolean toggle: sample - return actions.toggleSample(evt.target.checked); - } - } - - onApplyButtonClicked = (evt) => { - // No evt when pressing enter from ACE. - if (evt) { - evt.preventDefault(); - evt.stopPropagation(); - } - - this.props.onApply(); - }; - - onResetButtonClicked = () => { - this.props.onReset(); - }; - - getQueryOption( - label, - autoPopulated, - hasToggle, - hasError, - id, - value, - placeholder, - option - ) { - return ( - - ); - } - - _onFocus = () => { - this.setState({ hasFocus: true }); - }; - - _onBlur = () => { - this.setState({ hasFocus: false }); - }; - - _showToggle() { - return this.props.layout.length > 1; - } - - _queryHasChanges() { - const query = pick(this.props, QUERY_PROPERTIES); - return !isEqual(query, this.props.lastExecutedQuery); - } - - /** - * renders the options toggle button in the top right corner, if the layout - * is multi-line. - * - * @returns {Component|null} the toggle component or null. - */ - renderToggle() { - const { expanded, actions } = this.props; - - return this._showToggle() ? ( - - ) : null; - } - - /** - * renders a single query option, either as its own row, or as part of a - * option group. - * - * @param {String} option the option name to render - * @param {Number} id the option number - * @param {Boolean} hasToggle this option contains the expand toggle - * - * @return {Component} the option component - */ - renderOption(option, id, hasToggle) { - const { filterValid, autoPopulated } = this.props; - - // for filter only, also validate feature flag directives - const hasError = - option === 'filter' ? !filterValid : !this.props[`${option}Valid`]; - - // checkbox options use the value directly, text inputs use the - // `