From ffe99a4a7881ede9f84d3020b0084e4fe2f2a614 Mon Sep 17 00:00:00 2001 From: Alena Khineika Date: Thu, 18 Aug 2022 16:27:14 +0200 Subject: [PATCH 01/20] feat: implement new indexes modal designs COMPASS-5978 --- package-lock.json | 45 +- packages/compass-components/package.json | 2 +- .../components/collapsible-field-set.spec.tsx | 47 ++ .../src/components/collapsible-field-set.tsx | 80 +++ .../leafy-green/combobox/combobox.styles.ts | 7 + .../leafy-green/combobox/combobox.tsx | 15 +- packages/compass-components/src/index.ts | 2 + .../compass-e2e-tests/helpers/selectors.ts | 29 +- .../tests/collection-indexes-tab.test.ts | 1 - packages/compass-indexes/package.json | 2 +- .../create-index-field/create-index-field.jsx | 198 ------ .../create-index-field.module.less | 47 -- .../create-index-field.spec.jsx | 65 -- .../components/create-index-field/index.js | 2 - .../create-index-fields.spec.tsx | 76 ++ .../create-index-fields.tsx | 249 +++++++ .../components/create-index-fields/index.ts | 2 + .../columnstore-projection.tsx | 40 ++ .../create-index-form.spec.tsx | 535 ++++++++++++++ .../create-index-form/create-index-form.tsx | 202 ++++++ .../create-index-form/custom-collation.tsx | 41 ++ .../create-index-form/index-name.tsx | 38 + .../src/components/create-index-form/index.ts | 4 + .../partial-filter-expression.tsx | 38 + .../src/components/create-index-form/ttl.tsx | 39 + .../create-index-form/unique-index.tsx | 47 ++ .../create-index-form/wildcard-projection.tsx | 40 ++ .../create-index-modal/create-index-modal.jsx | 545 -------------- .../create-index-modal.module.less | 86 --- .../create-index-modal.spec.jsx | 666 ------------------ .../create-index-modal/create-index-modal.tsx | 198 ++++++ .../create-index-modal/{index.js => index.ts} | 0 .../modules/create-index/has-index-name.js | 37 + ...options.spec.js => has-index-name.spec.js} | 18 +- .../src/modules/create-index/index.js | 10 +- .../src/modules/create-index/show-options.js | 37 - packages/compass-indexes/src/typings.d.ts | 3 + .../src/utils/index-link-helper.ts | 5 +- .../src/components/sidebar.tsx | 72 +- 39 files changed, 1845 insertions(+), 1725 deletions(-) create mode 100644 packages/compass-components/src/components/collapsible-field-set.spec.tsx create mode 100644 packages/compass-components/src/components/collapsible-field-set.tsx delete mode 100644 packages/compass-indexes/src/components/create-index-field/create-index-field.jsx delete mode 100644 packages/compass-indexes/src/components/create-index-field/create-index-field.module.less delete mode 100644 packages/compass-indexes/src/components/create-index-field/create-index-field.spec.jsx delete mode 100644 packages/compass-indexes/src/components/create-index-field/index.js create mode 100644 packages/compass-indexes/src/components/create-index-fields/create-index-fields.spec.tsx create mode 100644 packages/compass-indexes/src/components/create-index-fields/create-index-fields.tsx create mode 100644 packages/compass-indexes/src/components/create-index-fields/index.ts create mode 100644 packages/compass-indexes/src/components/create-index-form/columnstore-projection.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/create-index-form.spec.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/create-index-form.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/custom-collation.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/index-name.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/index.ts create mode 100644 packages/compass-indexes/src/components/create-index-form/partial-filter-expression.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/ttl.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/unique-index.tsx create mode 100644 packages/compass-indexes/src/components/create-index-form/wildcard-projection.tsx delete mode 100644 packages/compass-indexes/src/components/create-index-modal/create-index-modal.jsx delete mode 100644 packages/compass-indexes/src/components/create-index-modal/create-index-modal.module.less delete mode 100644 packages/compass-indexes/src/components/create-index-modal/create-index-modal.spec.jsx create mode 100644 packages/compass-indexes/src/components/create-index-modal/create-index-modal.tsx rename packages/compass-indexes/src/components/create-index-modal/{index.js => index.ts} (100%) create mode 100644 packages/compass-indexes/src/modules/create-index/has-index-name.js rename packages/compass-indexes/src/modules/create-index/{show-options.spec.js => has-index-name.spec.js} (55%) delete mode 100644 packages/compass-indexes/src/modules/create-index/show-options.js diff --git a/package-lock.json b/package-lock.json index 3e78a2c1ece..f2b108e61eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5675,13 +5675,13 @@ } }, "node_modules/@leafygreen-ui/popover": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/popover/-/popover-7.2.2.tgz", - "integrity": "sha512-MtrRHBh5Rw3hY7aQRzPa9209HY2kVCtMY5nOqS3R4JQDqD5VoC5HfYAiSQ7BJRgI9GFZ81sEVfPIYZsNQoqXpg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/popover/-/popover-7.2.3.tgz", + "integrity": "sha512-PIUA3hIIVS/fp4hcmi7tpZV3KDmXm29IyZXKJsKN1duiLSO9mKaynmywG80iezPuRYKfeoCsVndnSPpoZX8Vng==", "dependencies": { "@leafygreen-ui/hooks": "^7.0.0", "@leafygreen-ui/leafygreen-provider": "^2.1.3", - "@leafygreen-ui/lib": "^9.0.0", + "@leafygreen-ui/lib": "^9.2.1", "@leafygreen-ui/portal": "^4.0.0", "lodash": "^4.17.21", "react-transition-group": "^4.4.1" @@ -9748,6 +9748,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-bootstrap": { + "version": "0.32.30", + "resolved": "https://registry.npmjs.org/@types/react-bootstrap/-/react-bootstrap-0.32.30.tgz", + "integrity": "sha512-VylYVY38aitDEyvM/h9IBmdwVzwRNcP2zJS2OA12M5lDzGSzzmYAoSaKnR3Ew7VN8xxw0mIlWEMhc2ZYPeeqWg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.10.tgz", @@ -63112,7 +63121,7 @@ "@leafygreen-ui/banner": "^3.0.9", "@leafygreen-ui/button": "^12.0.5", "@leafygreen-ui/card": "^5.1.4", - "@leafygreen-ui/checkbox": "^6.0.6", + "@leafygreen-ui/checkbox": "^7.0.1", "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/confirmation-modal": "^2.2.3", "@leafygreen-ui/emotion": "^4.0.0", @@ -70371,6 +70380,7 @@ "@mongodb-js/webpack-config-compass": "^1.0.0", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", + "@types/react-bootstrap": "^0.32.30", "chai": "^4.2.0", "classnames": "^2.2.6", "debug": "^4.2.0", @@ -70385,7 +70395,6 @@ "lodash.map": "^4.6.0", "lodash.max": "^4.0.1", "lodash.pick": "^4.4.0", - "lodash.pluck": "^3.1.2", "mocha": "^8.4.0", "mongodb-query-parser": "^2.4.6", "numeral": "^2.0.6", @@ -97746,13 +97755,13 @@ } }, "@leafygreen-ui/popover": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@leafygreen-ui/popover/-/popover-7.2.2.tgz", - "integrity": "sha512-MtrRHBh5Rw3hY7aQRzPa9209HY2kVCtMY5nOqS3R4JQDqD5VoC5HfYAiSQ7BJRgI9GFZ81sEVfPIYZsNQoqXpg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/popover/-/popover-7.2.3.tgz", + "integrity": "sha512-PIUA3hIIVS/fp4hcmi7tpZV3KDmXm29IyZXKJsKN1duiLSO9mKaynmywG80iezPuRYKfeoCsVndnSPpoZX8Vng==", "requires": { "@leafygreen-ui/hooks": "^7.0.0", "@leafygreen-ui/leafygreen-provider": "^2.1.3", - "@leafygreen-ui/lib": "^9.0.0", + "@leafygreen-ui/lib": "^9.2.1", "@leafygreen-ui/portal": "^4.0.0", "lodash": "^4.17.21", "react-transition-group": "^4.4.1" @@ -103443,7 +103452,7 @@ "@leafygreen-ui/banner": "^3.0.9", "@leafygreen-ui/button": "^12.0.5", "@leafygreen-ui/card": "^5.1.4", - "@leafygreen-ui/checkbox": "^6.0.6", + "@leafygreen-ui/checkbox": "^7.0.1", "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/confirmation-modal": "^2.2.3", "@leafygreen-ui/emotion": "^4.0.0", @@ -103515,7 +103524,8 @@ }, "dependencies": { "@leafygreen-ui/checkbox": { - "version": "https://registry.npmjs.org/@leafygreen-ui/checkbox/-/checkbox-7.0.1.tgz", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/checkbox/-/checkbox-7.0.1.tgz", "integrity": "sha512-KA96oQK8pQfJn49yuisQAmxCAj95m3U+6pFO8D2+DLzhDSQtISO8TOLCeT9Bvs/tbvOUnqGAc5UjRPhrAjHO+Q==", "requires": { "@leafygreen-ui/hooks": "^7.0.0", @@ -108322,6 +108332,7 @@ "@mongodb-js/webpack-config-compass": "^1.0.0", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", + "@types/react-bootstrap": "^0.32.30", "bson": "^4.4.1", "chai": "^4.2.0", "classnames": "^2.2.6", @@ -108339,7 +108350,6 @@ "lodash.map": "^4.6.0", "lodash.max": "^4.0.1", "lodash.pick": "^4.4.0", - "lodash.pluck": "^3.1.2", "mocha": "^8.4.0", "mongodb-index-model": "^4.0.0", "mongodb-query-parser": "^2.4.6", @@ -123431,6 +123441,15 @@ "csstype": "^3.0.2" } }, + "@types/react-bootstrap": { + "version": "0.32.30", + "resolved": "https://registry.npmjs.org/@types/react-bootstrap/-/react-bootstrap-0.32.30.tgz", + "integrity": "sha512-VylYVY38aitDEyvM/h9IBmdwVzwRNcP2zJS2OA12M5lDzGSzzmYAoSaKnR3Ew7VN8xxw0mIlWEMhc2ZYPeeqWg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.10.tgz", diff --git a/packages/compass-components/package.json b/packages/compass-components/package.json index 42d16f788ba..37de699b173 100644 --- a/packages/compass-components/package.json +++ b/packages/compass-components/package.json @@ -35,7 +35,7 @@ "@leafygreen-ui/banner": "^3.0.9", "@leafygreen-ui/button": "^12.0.5", "@leafygreen-ui/card": "^5.1.4", - "@leafygreen-ui/checkbox": "^6.0.6", + "@leafygreen-ui/checkbox": "^7.0.1", "@leafygreen-ui/code": "^9.4.0", "@leafygreen-ui/confirmation-modal": "^2.2.3", "@leafygreen-ui/emotion": "^4.0.0", diff --git a/packages/compass-components/src/components/collapsible-field-set.spec.tsx b/packages/compass-components/src/components/collapsible-field-set.spec.tsx new file mode 100644 index 00000000000..9ca226791e8 --- /dev/null +++ b/packages/compass-components/src/components/collapsible-field-set.spec.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { TextInput } from '@mongodb-js/compass-components'; + +import { render, screen, cleanup } from '@testing-library/react'; + +import { CollapsibleFieldSet } from './collapsible-field-set'; + +function collapsibleFieldSet(toggled: boolean) { + const onToggleSpy = sinon.spy(); + return ( + + + + ); +} + +describe('CollapsibleFieldSet Component', function () { + afterEach(cleanup); + + it('should not display a child input', function () { + render(collapsibleFieldSet(false)); + const checkbox = screen.getByTestId('my-checkbox'); + expect(checkbox).to.exist; + expect(screen.queryByTestId('my-input')).to.not.exist; + }); + + it('should display a child input', function () { + render(collapsibleFieldSet(true)); + const checkbox = screen.getByTestId('my-checkbox'); + expect(checkbox).to.exist; + expect(screen.queryByTestId('my-input')).to.exist; + }); +}); diff --git a/packages/compass-components/src/components/collapsible-field-set.tsx b/packages/compass-components/src/components/collapsible-field-set.tsx new file mode 100644 index 00000000000..1742f6e4e5b --- /dev/null +++ b/packages/compass-components/src/components/collapsible-field-set.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { css } from '@leafygreen-ui/emotion'; +import { Link, Checkbox, Label } from './leafygreen'; +import { useId } from '@react-aria/utils'; +import { spacing } from '@leafygreen-ui/tokens'; +import { withTheme } from '../hooks/use-theme'; +import { Description } from '@leafygreen-ui/typography'; + +const infoLinkStyles = css({ + marginLeft: spacing[1], +}); + +const collapsibleFieldsetStyles = css({ + margin: `${spacing[3]}px 0`, + fieldset: { + paddingLeft: `${spacing[4]}px`, + }, +}); + +export type CreateIndexProps = { + darkMode?: boolean; + dataTestId?: string; + children?: React.ReactElement; + label: string; + description?: React.ReactElement | string; + disabled?: boolean; + helpUrl?: string; + onToggle: (checked: boolean) => boolean; + toggled?: boolean; +}; + +const UnthemedCollapsibleFieldSet = ({ + darkMode, + description, + disabled, + helpUrl, + label, + onToggle, + toggled, + ...props +}: React.PropsWithChildren): React.ReactElement => { + const labelId = useId(); + return ( +
+ { + onToggle(event.target.checked); + }} + disabled={disabled} + label={} + description={ + !description + ? '' + : (( + + {description} + {!!helpUrl && ( + + Learn More + + )} + + ) as any) // LG Checkbox expects a string description, but we use Description component to include helpUrl. + } + checked={toggled} + id={labelId} + darkMode={darkMode} + /> + {toggled &&
{props.children}
} +
+ ); +}; + +const CollapsibleFieldSet = withTheme(UnthemedCollapsibleFieldSet); +export { CollapsibleFieldSet }; diff --git a/packages/compass-components/src/components/leafy-green/combobox/combobox.styles.ts b/packages/compass-components/src/components/leafy-green/combobox/combobox.styles.ts index 98e63dbd536..807f272bdf6 100644 --- a/packages/compass-components/src/components/leafy-green/combobox/combobox.styles.ts +++ b/packages/compass-components/src/components/leafy-green/combobox/combobox.styles.ts @@ -351,3 +351,10 @@ export const menuMessage = css` height: 1em; } `; + +export const popoverStyle = () => css` + overflow: hidden; + top: auto; + left: auto; + box-shadow: 0 3px 7px 0 rgba(6, 22, 33, 0.3); +`; diff --git a/packages/compass-components/src/components/leafy-green/combobox/combobox.tsx b/packages/compass-components/src/components/leafy-green/combobox/combobox.tsx index 232d50636f3..a2264f8e366 100644 --- a/packages/compass-components/src/components/leafy-green/combobox/combobox.tsx +++ b/packages/compass-components/src/components/leafy-green/combobox/combobox.tsx @@ -45,6 +45,7 @@ import { menuMessage, menuStyle, menuWrapperStyle, + popoverStyle, } from './combobox.styles'; import { InternalComboboxGroup } from './combobox-group'; import type { OptionObject } from './util'; @@ -885,13 +886,18 @@ export default function Combobox({ } setInputFocus(cursorPos); + // Open menu on click instead of on focus. + // https://jira.mongodb.org/browse/PD-2321 + openMenu(); } }; // Fired when the wrapper gains focus const handleInputWrapperFocus = () => { scrollToEnd(); - openMenu(); + // To prevent Combobox menu from automatically being opened on focus. + // https://jira.mongodb.org/browse/PD-2321 + // openMenu(); }; // Fired onChange @@ -1152,7 +1158,12 @@ export default function Combobox({ justify="middle" refEl={comboboxRef} adjustOnMutation={true} - className={menuWrapperStyle({ darkMode, size, width: menuWidth })} + className={cx( + popoverStyle(), + menuWrapperStyle({ darkMode, size, width: menuWidth }) + )} + usePortal={false} + popoverZIndex={999999} >
{ - return `[data-test-id="create-index-field-name-${idx}"] input`; + return `[data-testid="create-index-fields-name-${idx}"] input`; }; export const CreateIndexModalFieldTypeSelectButton = (idx: number): string => { - return `[data-test-id="create-index-field-type-${idx}"] button`; + return `[data-testid="create-index-fields-type-${idx}"] button`; }; export const CreateIndexModalFieldTypeSelectMenu = (idx: number): string => { - return `[data-test-id="create-index-field-type-${idx}"] #create-index-field-type-${idx}-menu`; + return `[data-testid="create-index-fields-type-${idx}"] #create-index-fields-type-select-${idx}-menu`; }; -export const CreateIndexConfirmButton = '[data-test-id="create-index-button"]'; + +export const IndexToggleOptions = + '[data-testid="create-index-modal-toggle-options"]'; +export const IndexToggleIsWildcard = + '[data-testid="create-index-modal-has-wildcard-checkbox"] input[type="checkbox"]'; +export const IndexWildcardProjectionEditor = + '[data-testid="create-index-modal-has-wildcard-checkbox"] .ace_editor'; + +export const CreateIndexErrorMessage = `${CreateIndexModal} [role="alert"]`; +export const CreateIndexConfirmButton = `${CreateIndexModal} [role=dialog] > div:nth-child(2) button:first-child`; +export const CreateIndexCancelButton = `${CreateIndexModal} [role=dialog] > div:nth-child(2) button:last-child`; export const DropIndexModal = '[data-testid="drop_index_modal"]'; export const DropIndexModalConfirmName = diff --git a/packages/compass-e2e-tests/tests/collection-indexes-tab.test.ts b/packages/compass-e2e-tests/tests/collection-indexes-tab.test.ts index 44d0856a346..abc539fa1ca 100644 --- a/packages/compass-e2e-tests/tests/collection-indexes-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-indexes-tab.test.ts @@ -65,7 +65,6 @@ describe('Collection indexes tab', function () { Selectors.CreateIndexModalFieldTypeSelectButton(0) ); await fieldTypeSelect.waitForDisplayed(); - await fieldTypeSelect.click(); const fieldTypeSelectMenu = await browser.$( diff --git a/packages/compass-indexes/package.json b/packages/compass-indexes/package.json index 786d16da060..a00355e4933 100644 --- a/packages/compass-indexes/package.json +++ b/packages/compass-indexes/package.json @@ -72,6 +72,7 @@ "@mongodb-js/webpack-config-compass": "^1.0.0", "@testing-library/react": "^12.1.4", "@testing-library/user-event": "^13.5.0", + "@types/react-bootstrap": "^0.32.30", "chai": "^4.2.0", "classnames": "^2.2.6", "debug": "^4.2.0", @@ -86,7 +87,6 @@ "lodash.map": "^4.6.0", "lodash.max": "^4.0.1", "lodash.pick": "^4.4.0", - "lodash.pluck": "^3.1.2", "mocha": "^8.4.0", "mongodb-query-parser": "^2.4.6", "numeral": "^2.0.6", diff --git a/packages/compass-indexes/src/components/create-index-field/create-index-field.jsx b/packages/compass-indexes/src/components/create-index-field/create-index-field.jsx deleted file mode 100644 index 0f00c949af5..00000000000 --- a/packages/compass-indexes/src/components/create-index-field/create-index-field.jsx +++ /dev/null @@ -1,198 +0,0 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; - -import styles from './create-index-field.module.less'; -import { hasColumnstoreIndexesSupport } from '../../utils/has-columnstore-indexes-support'; - -import { - Combobox, - ComboboxOption, - Select, - Option, -} from '@mongodb-js/compass-components'; - -/** - * Current allowed types for indexes. - */ -const INDEX_TYPES = ['1 (asc)', '-1 (desc)', '2dsphere', 'text', 'columnstore']; - -/** - * Default values for field name and type as presented in the UI - */ -const DEFAULT_FIELD = { - name: 'Select or type a field name', - type: 'Select a type', -}; - -/** - * Component for the index field form. - */ -class CreateIndexField extends PureComponent { - static displayName = 'CreateIndexField'; - - static propTypes = { - fields: PropTypes.array.isRequired, - field: PropTypes.object.isRequired, - idx: PropTypes.number.isRequired, - serverVersion: PropTypes.string.isRequired, - disabledFields: PropTypes.array.isRequired, - isRemovable: PropTypes.bool.isRequired, - addField: PropTypes.func.isRequired, - removeField: PropTypes.func.isRequired, - updateFieldName: PropTypes.func.isRequired, - updateFieldType: PropTypes.func.isRequired, - newIndexField: PropTypes.string, - createNewIndexField: PropTypes.func.isRequired, - }; - - /** - * Create React dropdown items for each element in the fields array. - * - * @returns {Array} The React components for each item in the field and type dropdowns. - */ - getDropdownFieldsSelect() { - return this.props.fields.map((elem) => ({ - value: elem, - label: elem, - disabled: this.props.disabledFields.some((field) => field === elem), - })); - } - - /** - * Create React dropdown items for each element in the INDEX_TYPES array. - * - * @returns {Array} The React components for each item in the field and type dropdowns. - */ - getDropdownTypes() { - if (!hasColumnstoreIndexesSupport(this.props.serverVersion)) { - return INDEX_TYPES.filter((elem) => elem !== 'columnstore').map( - (elem) => ( - - ) - ); - } - - return INDEX_TYPES.map((elem) => ( - - )); - } - - /** - * Set state to selected field on name change. - * - * @param {object} name - The selected field name. - */ - selectFieldName(name) { - if (name !== null) { - this.props.updateFieldName(this.props.idx, name); - } - } - - /** - * Set state to selected field on type change. - * - * @param {string} type - The selected field type. - */ - selectFieldType(type) { - this.props.updateFieldType(this.props.idx, type); - } - - /** - * Remove this index field - * - * @param {object} evt The click event. - */ - remove(evt) { - evt.preventDefault(); - evt.stopPropagation(); - this.props.removeField(this.props.idx); - } - - renderIndexOptions() { - const fields = this.props.fields.map((value, idx) => ( - - )); - - if ( - this.props.newIndexField && - !this.props.fields.includes(this.props.newIndexField) - ) { - const newIndexField = this.props.newIndexField; - - fields.push( - - ); - } - - return fields; - } - - /** - * Render the index field form. - * - * @returns {React.Component} The index field form. - */ - render() { - return ( -
-
- - {this.renderIndexOptions()} - -
-
- -
-
-