diff --git a/package-lock.json b/package-lock.json index f3118b62690..b6a71e10d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10182,26 +10182,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/@mongodb-js/diagramming": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/diagramming/-/diagramming-1.5.1.tgz", - "integrity": "sha512-lyF8VIh+hwFEmou980K4gB9f+PegMaXgFlgQijur4oRZlsIrlmvQ4Gg5r0C/SqVyMn7MQIDiADgZr+NJJ8sd6Q==", - "license": "MIT", - "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.0", - "@leafygreen-ui/icon": "^14.3.0", - "@leafygreen-ui/leafygreen-provider": "^5.0.2", - "@leafygreen-ui/palette": "^5.0.0", - "@leafygreen-ui/tokens": "^3.2.1", - "@leafygreen-ui/typography": "^22.1.0", - "@xyflow/react": "12.5.1", - "d3-path": "^3.1.0", - "elkjs": "^0.10.0", - "react": "17.0.2", - "react-dom": "17.0.2" - } - }, "node_modules/@mongodb-js/dl-center": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@mongodb-js/dl-center/-/dl-center-1.3.0.tgz", @@ -49142,7 +49122,7 @@ "@mongodb-js/compass-user-data": "^0.10.0", "@mongodb-js/compass-utils": "^0.9.15", "@mongodb-js/compass-workspaces": "^0.57.0", - "@mongodb-js/diagramming": "^1.5.1", + "@mongodb-js/diagramming": "^1.6.0", "bson": "^6.10.4", "compass-preferences-model": "^2.55.0", "html-to-image": "1.11.11", @@ -49177,6 +49157,101 @@ "xvfb-maybe": "^0.2.1" } }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/emotion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/emotion/-/emotion-5.0.2.tgz", + "integrity": "sha512-pnAWIg7iFZUtpUkhhFATyx+p2qzySKmWrqGNp409+v6EgkfbpQkZIr+52fDzU4Xzc8PgGT+yBg9CTw6W4yzumg==", + "license": "Apache-2.0", + "dependencies": { + "@emotion/css": "^11.1.3", + "@emotion/server": "^11.4.0" + } + }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/hooks": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-9.1.3.tgz", + "integrity": "sha512-M3fKuSiz+mEmIi1d8dK5pPbwbELbJFJsonRH+iTHM3vJPTo6w+bCws1g/sYiP7UryogZLogM9/i8NL6nejhRog==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/lib": "^15.3.0", + "@leafygreen-ui/tokens": "^3.2.4", + "lodash": "^4.17.21" + } + }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/icon": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/icon/-/icon-14.5.0.tgz", + "integrity": "sha512-dZIrwNxTJrDp12OSuk8TU5dzrWSK38zyyL8dZcdXpo3VtWaWnzwZDK6tr08dt+Tgj7Dj2Qchg7+iwotHHbv3Lg==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/emotion": "^5.0.2", + "lodash": "^4.17.21" + } + }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/leafygreen-provider": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/leafygreen-provider/-/leafygreen-provider-5.0.4.tgz", + "integrity": "sha512-VDlmjTiIqlITVhq4VKUDq8FLySWnHkTxSV2n1sxOanLNPuatOXjxsPmCkPUBXhmQKk/fBf4yQnDKOwJvkyzE6Q==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/hooks": "^9.1.3", + "@leafygreen-ui/lib": "^15.3.0", + "react-transition-group": "^4.4.5" + } + }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/palette": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/palette/-/palette-5.0.2.tgz", + "integrity": "sha512-+PrfGeJSv4goxm/vKpfJJDOP7t/uElj+14K8jiIyu3qR3TcFRIZ5h1VMvICTUgqvRc8W+xIZYQwsLa2XCu2lvw==", + "license": "Apache-2.0" + }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/polymorphic": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/polymorphic/-/polymorphic-3.0.4.tgz", + "integrity": "sha512-PRpEkgJgke5jEaM4Q3heRzfsLc0HKnW4p7YiXlQxIe0qQcUOYSHiUeBBqh4UwgX7oMz699ZDM/sLC4hp/aNl7Q==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/lib": "^15.3.0", + "lodash": "^4.17.21" + } + }, + "packages/compass-data-modeling/node_modules/@leafygreen-ui/typography": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/typography/-/typography-22.1.2.tgz", + "integrity": "sha512-sULk4TVstU2Zk1l5RZFkkgP7BomxjDX4P3/aFs/2L9kVHYxGBvtykZb3SALPNUSP1L9+U1KDe5qmSckqO2hbmA==", + "license": "Apache-2.0", + "dependencies": { + "@leafygreen-ui/emotion": "^5.0.2", + "@leafygreen-ui/icon": "^14.5.0", + "@leafygreen-ui/lib": "^15.3.0", + "@leafygreen-ui/palette": "^5.0.2", + "@leafygreen-ui/polymorphic": "^3.0.4", + "@leafygreen-ui/tokens": "^3.2.4" + }, + "peerDependencies": { + "@leafygreen-ui/leafygreen-provider": "^5.0.4" + } + }, + "packages/compass-data-modeling/node_modules/@mongodb-js/diagramming": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/diagramming/-/diagramming-1.6.0.tgz", + "integrity": "sha512-/8Q9kBkXcj16wotHKszGJkoy6wK8lDiMaIcL2Nv2FeXdZP/EfirfhaxTzMYJw7jHM9TaYD6Wl4YNzqdnlrYhTA==", + "license": "MIT", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@leafygreen-ui/icon": "^14.3.0", + "@leafygreen-ui/leafygreen-provider": "^5.0.2", + "@leafygreen-ui/palette": "^5.0.0", + "@leafygreen-ui/tokens": "^3.2.1", + "@leafygreen-ui/typography": "^22.1.0", + "@xyflow/react": "12.5.1", + "d3-path": "^3.1.0", + "elkjs": "^0.10.0", + "react": "17.0.2", + "react-dom": "17.0.2" + } + }, "packages/compass-data-modeling/node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -62819,7 +62894,7 @@ "@mongodb-js/compass-user-data": "^0.10.0", "@mongodb-js/compass-utils": "^0.9.15", "@mongodb-js/compass-workspaces": "^0.57.0", - "@mongodb-js/diagramming": "^1.5.1", + "@mongodb-js/diagramming": "^1.6.0", "@mongodb-js/eslint-config-compass": "^1.4.10", "@mongodb-js/mocha-config-compass": "^1.7.1", "@mongodb-js/prettier-config-compass": "^1.2.8", @@ -62852,6 +62927,90 @@ "xvfb-maybe": "^0.2.1" }, "dependencies": { + "@leafygreen-ui/emotion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/emotion/-/emotion-5.0.2.tgz", + "integrity": "sha512-pnAWIg7iFZUtpUkhhFATyx+p2qzySKmWrqGNp409+v6EgkfbpQkZIr+52fDzU4Xzc8PgGT+yBg9CTw6W4yzumg==", + "requires": { + "@emotion/css": "^11.1.3", + "@emotion/server": "^11.4.0" + } + }, + "@leafygreen-ui/hooks": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/hooks/-/hooks-9.1.3.tgz", + "integrity": "sha512-M3fKuSiz+mEmIi1d8dK5pPbwbELbJFJsonRH+iTHM3vJPTo6w+bCws1g/sYiP7UryogZLogM9/i8NL6nejhRog==", + "requires": { + "@leafygreen-ui/lib": "^15.3.0", + "@leafygreen-ui/tokens": "^3.2.4", + "lodash": "^4.17.21" + } + }, + "@leafygreen-ui/icon": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/icon/-/icon-14.5.0.tgz", + "integrity": "sha512-dZIrwNxTJrDp12OSuk8TU5dzrWSK38zyyL8dZcdXpo3VtWaWnzwZDK6tr08dt+Tgj7Dj2Qchg7+iwotHHbv3Lg==", + "requires": { + "@leafygreen-ui/emotion": "^5.0.2", + "lodash": "^4.17.21" + } + }, + "@leafygreen-ui/leafygreen-provider": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/leafygreen-provider/-/leafygreen-provider-5.0.4.tgz", + "integrity": "sha512-VDlmjTiIqlITVhq4VKUDq8FLySWnHkTxSV2n1sxOanLNPuatOXjxsPmCkPUBXhmQKk/fBf4yQnDKOwJvkyzE6Q==", + "requires": { + "@leafygreen-ui/hooks": "^9.1.3", + "@leafygreen-ui/lib": "^15.3.0", + "react-transition-group": "^4.4.5" + } + }, + "@leafygreen-ui/palette": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/palette/-/palette-5.0.2.tgz", + "integrity": "sha512-+PrfGeJSv4goxm/vKpfJJDOP7t/uElj+14K8jiIyu3qR3TcFRIZ5h1VMvICTUgqvRc8W+xIZYQwsLa2XCu2lvw==" + }, + "@leafygreen-ui/polymorphic": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/polymorphic/-/polymorphic-3.0.4.tgz", + "integrity": "sha512-PRpEkgJgke5jEaM4Q3heRzfsLc0HKnW4p7YiXlQxIe0qQcUOYSHiUeBBqh4UwgX7oMz699ZDM/sLC4hp/aNl7Q==", + "requires": { + "@leafygreen-ui/lib": "^15.3.0", + "lodash": "^4.17.21" + } + }, + "@leafygreen-ui/typography": { + "version": "22.1.2", + "resolved": "https://registry.npmjs.org/@leafygreen-ui/typography/-/typography-22.1.2.tgz", + "integrity": "sha512-sULk4TVstU2Zk1l5RZFkkgP7BomxjDX4P3/aFs/2L9kVHYxGBvtykZb3SALPNUSP1L9+U1KDe5qmSckqO2hbmA==", + "requires": { + "@leafygreen-ui/emotion": "^5.0.2", + "@leafygreen-ui/icon": "^14.5.0", + "@leafygreen-ui/lib": "^15.3.0", + "@leafygreen-ui/palette": "^5.0.2", + "@leafygreen-ui/polymorphic": "^3.0.4", + "@leafygreen-ui/tokens": "^3.2.4" + } + }, + "@mongodb-js/diagramming": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/diagramming/-/diagramming-1.6.0.tgz", + "integrity": "sha512-/8Q9kBkXcj16wotHKszGJkoy6wK8lDiMaIcL2Nv2FeXdZP/EfirfhaxTzMYJw7jHM9TaYD6Wl4YNzqdnlrYhTA==", + "requires": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@leafygreen-ui/icon": "^14.3.0", + "@leafygreen-ui/leafygreen-provider": "^5.0.2", + "@leafygreen-ui/palette": "^5.0.0", + "@leafygreen-ui/tokens": "^3.2.1", + "@leafygreen-ui/typography": "^22.1.0", + "@xyflow/react": "12.5.1", + "d3-path": "^3.1.0", + "elkjs": "^0.10.0", + "react": "17.0.2", + "react-dom": "17.0.2" + } + }, "@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -65798,25 +65957,6 @@ } } }, - "@mongodb-js/diagramming": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mongodb-js/diagramming/-/diagramming-1.5.1.tgz", - "integrity": "sha512-lyF8VIh+hwFEmou980K4gB9f+PegMaXgFlgQijur4oRZlsIrlmvQ4Gg5r0C/SqVyMn7MQIDiADgZr+NJJ8sd6Q==", - "requires": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.0", - "@leafygreen-ui/icon": "^13.1.2", - "@leafygreen-ui/leafygreen-provider": "^4.0.2", - "@leafygreen-ui/palette": "^4.1.3", - "@leafygreen-ui/tokens": "^3.2.4", - "@leafygreen-ui/typography": "^20.0.2", - "@xyflow/react": "12.5.1", - "d3-path": "^3.1.0", - "elkjs": "^0.10.0", - "react": "^17.0.2", - "react-dom": "^17.0.2" - } - }, "@mongodb-js/dl-center": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@mongodb-js/dl-center/-/dl-center-1.3.0.tgz", diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index ffacf0f8226..ebba71cc124 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -97,7 +97,11 @@ export { VisuallyHidden } from '@react-aria/visually-hidden'; export { openToast, closeToast, ToastArea } from './hooks/use-toast'; -export { breakpoints, spacing } from '@leafygreen-ui/tokens'; +export { + breakpoints, + spacing, + transitionDuration, +} from '@leafygreen-ui/tokens'; import IndexIcon from './components/index-icon'; export { default as FormFieldContainer } from './components/form-field-container'; diff --git a/packages/compass-data-modeling/package.json b/packages/compass-data-modeling/package.json index 026e61c2aca..8769239a083 100644 --- a/packages/compass-data-modeling/package.json +++ b/packages/compass-data-modeling/package.json @@ -63,7 +63,7 @@ "@mongodb-js/compass-user-data": "^0.10.0", "@mongodb-js/compass-utils": "^0.9.15", "@mongodb-js/compass-workspaces": "^0.57.0", - "@mongodb-js/diagramming": "^1.5.1", + "@mongodb-js/diagramming": "^1.6.0", "bson": "^6.10.4", "compass-preferences-model": "^2.55.0", "html-to-image": "1.11.11", diff --git a/packages/compass-data-modeling/src/components/diagram-editor.tsx b/packages/compass-data-modeling/src/components/diagram-editor.tsx index 6f9bb483649..8de646153ea 100644 --- a/packages/compass-data-modeling/src/components/diagram-editor.tsx +++ b/packages/compass-data-modeling/src/components/diagram-editor.tsx @@ -9,6 +9,7 @@ import { connect } from 'react-redux'; import type { DataModelingState } from '../store/reducer'; import { addNewFieldToCollection, + onAddNestedField, moveCollection, selectCollection, selectRelationship, @@ -113,6 +114,7 @@ const DiagramContent: React.FunctionComponent<{ editErrors?: string[]; newCollection?: string; onAddNewFieldToCollection: (ns: string) => void; + onAddNestedField: (ns: string, parentFieldPath: string[]) => void; onMoveCollection: (ns: string, newPosition: [number, number]) => void; onCollectionSelect: (namespace: string) => void; onRelationshipSelect: (rId: string) => void; @@ -133,6 +135,7 @@ const DiagramContent: React.FunctionComponent<{ isInRelationshipDrawingMode, newCollection, onAddNewFieldToCollection, + onAddNestedField, onMoveCollection, onCollectionSelect, onRelationshipSelect, @@ -183,6 +186,8 @@ const DiagramContent: React.FunctionComponent<{ : undefined, onClickAddNewFieldToCollection: () => onAddNewFieldToCollection(coll.ns), + onClickAddNestedField: (parentFieldPath: string[]) => + onAddNestedField(coll.ns, parentFieldPath), selected, isInRelationshipDrawingMode, }); @@ -193,6 +198,7 @@ const DiagramContent: React.FunctionComponent<{ model?.relationships, selectedItems, isInRelationshipDrawingMode, + onAddNestedField, ]); // Fit to view on initial mount @@ -309,6 +315,7 @@ const ConnectedDiagramContent = connect( }, { onAddNewFieldToCollection: addNewFieldToCollection, + onAddNestedField: onAddNestedField, onMoveCollection: moveCollection, onCollectionSelect: selectCollection, onRelationshipSelect: selectRelationship, diff --git a/packages/compass-data-modeling/src/components/diagram/object-field-type.tsx b/packages/compass-data-modeling/src/components/diagram/object-field-type.tsx new file mode 100644 index 00000000000..01042f89ba0 --- /dev/null +++ b/packages/compass-data-modeling/src/components/diagram/object-field-type.tsx @@ -0,0 +1,123 @@ +import React, { useCallback } from 'react'; +import { + css, + cx, + focusRing, + palette, + spacing, + transitionDuration, + transparentize, + useDarkMode, +} from '@mongodb-js/compass-components'; + +import PlusWithSquare from '../icons/plus-with-square'; + +const objectTypeContainerStyles = css({ + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', +}); + +const iconButtonHoverStyles = css({ + color: palette.gray.dark1, + + '&::before': { + content: '""', + transition: `${transitionDuration.default}ms all ease-in-out`, + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + borderRadius: '100%', + transform: 'scale(0.8)', + }, + + [`&:active:before, + &:hover:before, + &:focus:before, + &[data-hover='true']:before, + &[data-focus='true']:before`]: { + transform: 'scale(1)', + }, + + [`&:active, + &:hover, + &[data-hover='true'], + &:focus-visible, + &[data-focus='true']`]: { + color: palette.black, + + '&::before': { + backgroundColor: transparentize(0.9, palette.gray.dark2), + }, + }, +}); + +const iconButtonHoverDarkModeStyles = css({ + color: palette.gray.light1, + + [`&:active, + &:hover, + &[data-hover='true'], + &:focus-visible, + &[data-focus='true']`]: { + color: palette.gray.light3, + + '&::before': { + backgroundColor: transparentize(0.9, palette.gray.light2), + }, + }, +}); + +const addNestedFieldStyles = css(iconButtonHoverStyles, focusRing, { + background: 'none', + border: 'none', + padding: spacing[100], + margin: 0, + marginLeft: spacing[100], + cursor: 'pointer', + color: 'inherit', + display: 'flex', +}); + +type ObjectFieldTypeProps = { + onClickAddNestedField: (event: React.MouseEvent) => void; + ['data-testid']: string; +}; + +const ObjectFieldType: React.FunctionComponent = ({ + 'data-testid': dataTestId, + onClickAddNestedField: _onClickAddNestedField, +}) => { + const darkMode = useDarkMode(); + + const onClickAddNestedField = useCallback( + (event: React.MouseEvent) => { + // Don't click on the field element. + event.stopPropagation(); + _onClickAddNestedField(event); + }, + [_onClickAddNestedField] + ); + + return ( +
+ {'{}'} + +
+ ); +}; + +export { ObjectFieldType }; diff --git a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx index 13d6fa16228..8d8e4069c0f 100644 --- a/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx +++ b/packages/compass-data-modeling/src/components/drawer/diagram-editor-side-panel.spec.tsx @@ -171,7 +171,7 @@ describe('DiagramEditorSidePanel', function () { expect(nameInput).to.be.visible; expect(nameInput).to.have.value('alias'); - const selectedTypes = getMultiComboboxValues('lg-combobox-datatype'); + const selectedTypes = getMultiComboboxValues('field-type-combobox'); expect(selectedTypes).to.have.lengthOf(2); expect(selectedTypes).to.include('string'); expect(selectedTypes).to.include('int'); @@ -190,7 +190,7 @@ describe('DiagramEditorSidePanel', function () { expect(nameInput).to.be.visible; expect(nameInput).to.have.value('_id'); - const selectedTypes = getMultiComboboxValues('lg-combobox-datatype'); + const selectedTypes = getMultiComboboxValues('field-type-combobox'); expect(selectedTypes).to.have.lengthOf(1); expect(selectedTypes).to.include('string'); }); @@ -286,9 +286,7 @@ describe('DiagramEditorSidePanel', function () { expect(screen.getByTitle('routes.airline.name')).to.be.visible; // before - string - const selectedTypesBefore = getMultiComboboxValues( - 'lg-combobox-datatype' - ); + const selectedTypesBefore = getMultiComboboxValues('field-type-combobox'); expect(selectedTypesBefore).to.have.members(['string']); // add int and bool and remove string @@ -317,9 +315,7 @@ describe('DiagramEditorSidePanel', function () { expect(screen.getByTitle('routes.airline.name')).to.be.visible; // before - string - const selectedTypesBefore = getMultiComboboxValues( - 'lg-combobox-datatype' - ); + const selectedTypesBefore = getMultiComboboxValues('field-type-combobox'); expect(selectedTypesBefore).to.have.members(['string']); // remove string without adding anything else diff --git a/packages/compass-data-modeling/src/components/drawer/field-drawer-content.tsx b/packages/compass-data-modeling/src/components/drawer/field-drawer-content.tsx index 226e6937d2b..1af0d1f9a1f 100644 --- a/packages/compass-data-modeling/src/components/drawer/field-drawer-content.tsx +++ b/packages/compass-data-modeling/src/components/drawer/field-drawer-content.tsx @@ -172,7 +172,7 @@ const FieldDrawerContent: React.FunctionComponent = ({ { + return (dispatch, getState) => { + const modelState = selectCurrentModelFromState(getState()); + + const collection = modelState.collections.find((c) => c.ns === ns); + if (!collection) { + throw new Error('Collection to add field to not found'); + } + + const edit: Omit< + Extract, + 'id' | 'timestamp' + > = { + type: 'AddField', + ns, + // Use the first unique field name we can use. + field: [ + ...parentFieldPath, + getNewUnusedFieldName(collection.jsonSchema, parentFieldPath), + ], + jsonSchema: { + bsonType: 'string', + }, + }; + + return dispatch(applyEdit(edit)); + }; +} + export function moveCollection( ns: string, newPosition: [number, number] diff --git a/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.tsx b/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.tsx index 17803c39acb..235554338eb 100644 --- a/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.tsx +++ b/packages/compass-data-modeling/src/utils/nodes-and-edges.spec.tsx @@ -6,26 +6,116 @@ import { render, userEvent, } from '@mongodb-js/testing-library-compass'; -import { getFieldsFromSchema } from './nodes-and-edges'; +import type { NodeProps } from '@mongodb-js/diagramming'; -describe('getFieldsFromSchema', function () { - const validateMixedType = async ( - type: React.ReactNode, - expectedTooltip: RegExp - ) => { - render(<>{type}); - const mixed = screen.getByText('(mixed)'); - expect(mixed).to.be.visible; - expect(screen.queryByText(expectedTooltip)).to.not.exist; - userEvent.hover(mixed); - await waitFor(() => { - expect(screen.getByText(expectedTooltip)).to.be.visible; +import { + getFieldsFromSchema, + getBaseFieldsFromSchema, +} from './nodes-and-edges'; + +const validateMixedType = async ( + type: React.ReactNode, + expectedTooltip: RegExp +) => { + render(<>{type}); + const mixed = screen.getByText('(mixed)'); + expect(mixed).to.be.visible; + expect(screen.queryByText(expectedTooltip)).to.not.exist; + userEvent.hover(mixed); + await waitFor(() => { + expect(screen.getByText(expectedTooltip)).to.be.visible; + }); +}; + +function withoutObjectReactType(fields: NodeProps['fields']) { + return fields.map((f) => ({ + ...f, + type: React.isValidElement(f.type) ? 'object' : f.type, + })); +} + +describe('getBaseFieldsFromSchema', function () { + describe('flat schema', function () { + it('return empty array for empty schema', function () { + const result = getBaseFieldsFromSchema({ jsonSchema: {} }); + expect(result).to.deep.equal([]); }); - }; + it('returns fields for a simple schema', function () { + const result = getBaseFieldsFromSchema({ + jsonSchema: { + bsonType: 'object', + properties: { + name: { bsonType: 'string' }, + age: { bsonType: 'int' }, + }, + }, + }); + expect(result).to.deep.equal([ + { + name: 'name', + id: ['name'], + depth: 0, + }, + { + name: 'age', + id: ['age'], + depth: 0, + }, + ]); + }); + + it('returns fields for an array of mixed (including objects)', function () { + const result = getBaseFieldsFromSchema({ + jsonSchema: { + bsonType: 'object', + properties: { + todos: { + bsonType: 'array', + items: { + anyOf: [ + { + bsonType: 'object', + properties: { + title: { bsonType: 'string' }, + completed: { bsonType: 'boolean' }, + }, + }, + { bsonType: 'string' }, + ], + }, + }, + }, + }, + }); + expect(result).to.deep.equal([ + { + name: 'todos', + id: ['todos'], + depth: 0, + }, + { + name: 'title', + id: ['todos', 'title'], + depth: 1, + }, + { + name: 'completed', + id: ['todos', 'completed'], + depth: 1, + }, + ]); + }); + }); +}); + +describe('getFieldsFromSchema', function () { describe('flat schema', function () { it('return empty array for empty schema', function () { - const result = getFieldsFromSchema({ jsonSchema: {} }); + const result = getFieldsFromSchema({ + jsonSchema: {}, + onClickAddNestedField: () => {}, + }); expect(result).to.deep.equal([]); }); @@ -38,6 +128,7 @@ describe('getFieldsFromSchema', function () { age: { bsonType: 'int' }, }, }, + onClickAddNestedField: () => {}, }); expect(result).to.deep.equal([ { @@ -71,6 +162,7 @@ describe('getFieldsFromSchema', function () { age: { bsonType: ['int', 'string'] }, }, }, + onClickAddNestedField: () => {}, }); expect(result[0]).to.deep.include({ name: 'age', @@ -95,6 +187,7 @@ describe('getFieldsFromSchema', function () { }, }, highlightedFields: [['age']], + onClickAddNestedField: () => {}, }); expect(result).to.deep.equal([ { @@ -141,6 +234,7 @@ describe('getFieldsFromSchema', function () { }, }, highlightedFields: [['age'], ['profession']], + onClickAddNestedField: () => {}, }); expect(result).to.deep.equal([ { @@ -198,8 +292,9 @@ describe('getFieldsFromSchema', function () { }, }, }, + onClickAddNestedField: () => {}, }); - expect(result).to.deep.equal([ + expect(withoutObjectReactType(result)).to.deep.equal([ { name: 'person', id: ['person'], @@ -274,8 +369,9 @@ describe('getFieldsFromSchema', function () { }, }, highlightedFields: [['person', 'address', 'street']], + onClickAddNestedField: () => {}, }); - expect(result).to.deep.equal([ + expect(withoutObjectReactType(result)).to.deep.equal([ { name: 'person', id: ['person'], @@ -360,8 +456,9 @@ describe('getFieldsFromSchema', function () { ['person', 'address', 'street'], ['person', 'billingAddress', 'city'], ], + onClickAddNestedField: () => {}, }); - expect(result).to.deep.equal([ + expect(withoutObjectReactType(result)).to.deep.equal([ { name: 'person', id: ['person'], @@ -456,6 +553,7 @@ describe('getFieldsFromSchema', function () { }, }, }, + onClickAddNestedField: () => {}, }); expect(result).to.deep.equal([ { @@ -488,6 +586,7 @@ describe('getFieldsFromSchema', function () { }, }, }, + onClickAddNestedField: () => {}, }); expect(result).to.deep.equal([ { @@ -542,6 +641,7 @@ describe('getFieldsFromSchema', function () { }, }, }, + onClickAddNestedField: () => {}, }); expect(result).to.have.lengthOf(3); expect(result[0]).to.deep.include({ @@ -598,6 +698,7 @@ describe('getFieldsFromSchema', function () { }, }, }, + onClickAddNestedField: () => {}, }); expect(result).to.deep.equal([ { diff --git a/packages/compass-data-modeling/src/utils/nodes-and-edges.tsx b/packages/compass-data-modeling/src/utils/nodes-and-edges.tsx index fb49b7f4977..aad57d5dd6e 100644 --- a/packages/compass-data-modeling/src/utils/nodes-and-edges.tsx +++ b/packages/compass-data-modeling/src/utils/nodes-and-edges.tsx @@ -5,8 +5,14 @@ import { IconButton, InlineDefinition, css, + spacing, } from '@mongodb-js/compass-components'; -import type { NodeProps, EdgeProps, BaseNode } from '@mongodb-js/diagramming'; +import type { + NodeField, + NodeProps, + EdgeProps, + BaseNode, +} from '@mongodb-js/diagramming'; import type { MongoDBJSONSchema } from 'mongodb-schema'; import type { SelectedItems } from '../store/diagram'; import type { @@ -17,6 +23,7 @@ import type { import { traverseSchema } from './schema-traversal'; import { areFieldPathsEqual } from './utils'; import PlusWithSquare from '../components/icons/plus-with-square'; +import { ObjectFieldType } from '../components/diagram/object-field-type'; function getBsonTypeName(bsonType: string) { switch (bsonType) { @@ -29,6 +36,7 @@ function getBsonTypeName(bsonType: string) { const addNewFieldStyles = css({ marginLeft: 'auto', + marginRight: spacing[100], }); const mixedTypeTooltipContentStyles = css({ @@ -37,12 +45,30 @@ const mixedTypeTooltipContentStyles = css({ textAlign: 'left', }); -function getFieldTypeDisplay(bsonTypes: string[]) { +function getFieldTypeDisplay({ + bsonTypes, + onClickAddNestedField, + typeDisplayTestId, +}: { + bsonTypes: string[]; + onClickAddNestedField: () => void; + typeDisplayTestId: string; +}) { if (bsonTypes.length === 0) { return 'unknown'; } if (bsonTypes.length === 1) { + if (bsonTypes[0] === 'object') { + // Custom renderer for object types to include the "add field" button. + return ( + + ); + } + return getBsonTypeName(bsonTypes[0]); } @@ -84,34 +110,48 @@ export const getHighlightedFields = ( return selection; }; +const getBaseNodeField = (fieldPath: string[]): NodeField => { + return { + name: fieldPath[fieldPath.length - 1], + id: fieldPath, + depth: fieldPath.length - 1, + }; +}; + export const getFieldsFromSchema = ({ jsonSchema, highlightedFields = [], selectedField, + onClickAddNestedField, }: { jsonSchema: MongoDBJSONSchema; highlightedFields?: FieldPath[]; selectedField?: FieldPath; -}): NodeProps['fields'] => { + onClickAddNestedField: (parentFieldPath: string[]) => void; +}): NodeField[] => { if (!jsonSchema || !jsonSchema.properties) { return []; } - const fields: NodeProps['fields'] = []; + const fields: NodeField[] = []; traverseSchema({ jsonSchema, visitor: ({ fieldPath, fieldTypes }) => { fields.push({ - name: fieldPath[fieldPath.length - 1], - id: fieldPath, - type: getFieldTypeDisplay(fieldTypes), - depth: fieldPath.length - 1, + ...getBaseNodeField(fieldPath), + type: getFieldTypeDisplay({ + bsonTypes: fieldTypes, + typeDisplayTestId: `data-model-field-type-${fieldPath.join('-')}`, // Could have duplications, that's okay for test ids. + onClickAddNestedField: () => onClickAddNestedField(fieldPath), + }), glyphs: fieldTypes.length === 1 && fieldTypes[0] === 'objectId' ? ['key'] : [], selectable: true, - selected: areFieldPathsEqual(fieldPath, selectedField ?? []), + selected: + !!selectedField?.length && + areFieldPathsEqual(fieldPath, selectedField), variant: highlightedFields.length && highlightedFields.some((highlightedField) => @@ -126,6 +166,29 @@ export const getFieldsFromSchema = ({ return fields; }; +/** + * Create the base field list to be used for positioning and measuring in node layouts. + */ +export const getBaseFieldsFromSchema = ({ + jsonSchema, +}: { + jsonSchema: MongoDBJSONSchema; +}): NodeField[] => { + if (!jsonSchema || !jsonSchema.properties) { + return []; + } + const fields: NodeField[] = []; + + traverseSchema({ + jsonSchema, + visitor: ({ fieldPath }) => { + fields.push(getBaseNodeField(fieldPath)); + }, + }); + + return fields; +}; + /** * Create a base node to be used for positioning and measuring in node layouts. */ @@ -143,7 +206,7 @@ export function collectionToBaseNodeForLayout({ x: displayPosition[0], y: displayPosition[1], }, - fields: getFieldsFromSchema({ jsonSchema }), + fields: getBaseFieldsFromSchema({ jsonSchema }), }; } @@ -156,6 +219,7 @@ type CollectionWithRenderOptions = Pick< selected: boolean; isInRelationshipDrawingMode: boolean; onClickAddNewFieldToCollection: () => void; + onClickAddNestedField: (parentFieldPath: string[]) => void; }; export function collectionToDiagramNode({ @@ -167,6 +231,7 @@ export function collectionToDiagramNode({ selected, isInRelationshipDrawingMode, onClickAddNewFieldToCollection, + onClickAddNestedField, }: CollectionWithRenderOptions): NodeProps { return { id: ns, @@ -177,9 +242,10 @@ export function collectionToDiagramNode({ }, title: toNS(ns).collection, fields: getFieldsFromSchema({ - jsonSchema: jsonSchema, - highlightedFields: highlightedFields[ns] ?? undefined, + jsonSchema, + highlightedFields: highlightedFields[ns], selectedField, + onClickAddNestedField, }), selected, connectable: isInRelationshipDrawingMode, @@ -188,6 +254,7 @@ export function collectionToDiagramNode({ ) => { event.stopPropagation(); onClickAddNewFieldToCollection(); diff --git a/packages/compass-data-modeling/src/utils/schema.spec.ts b/packages/compass-data-modeling/src/utils/schema.spec.ts index e6a923b1f92..2ed09eb8bcd 100644 --- a/packages/compass-data-modeling/src/utils/schema.spec.ts +++ b/packages/compass-data-modeling/src/utils/schema.spec.ts @@ -34,6 +34,30 @@ describe('schema diagram utils', function () { const newFieldName = getNewUnusedFieldName(jsonSchema); expect(newFieldName).to.equal('field-3'); }); + + it('should return a new unused field name in a nested object', function () { + const jsonSchema = { + bsonType: 'object', + properties: { + a: { + bsonType: 'object', + properties: { + 'field-1': { + bsonType: 'string', + }, + 'field-2': { + bsonType: 'string', + }, + }, + }, + b: { + bsonType: 'string', + }, + }, + }; + const newFieldName = getNewUnusedFieldName(jsonSchema, ['a']); + expect(newFieldName).to.equal('field-3'); + }); }); describe('#addFieldToJSONSchema', function () { diff --git a/packages/compass-data-modeling/src/utils/schema.ts b/packages/compass-data-modeling/src/utils/schema.ts index 71e6332663f..db545a13b36 100644 --- a/packages/compass-data-modeling/src/utils/schema.ts +++ b/packages/compass-data-modeling/src/utils/schema.ts @@ -1,7 +1,21 @@ import type { MongoDBJSONSchema } from 'mongodb-schema'; -export function getNewUnusedFieldName(jsonSchema: MongoDBJSONSchema): string { - const existingFieldNames = new Set(Object.keys(jsonSchema.properties || {})); +export function getNewUnusedFieldName( + jsonSchema: MongoDBJSONSchema, + parentFieldPath: string[] = [] +): string { + let parentJSONSchema: MongoDBJSONSchema | undefined = jsonSchema; + for (const currentField of parentFieldPath) { + if (!currentField) { + throw new Error('Invalid field path to get new field name'); + } + parentJSONSchema = parentJSONSchema?.properties?.[currentField]; + } + + const existingFieldNames = new Set( + Object.keys(parentJSONSchema?.properties || {}) + ); + let i = 1; let fieldName = `field-${i}`; diff --git a/packages/compass-e2e-tests/helpers/commands/index.ts b/packages/compass-e2e-tests/helpers/commands/index.ts index 3d08f092c1e..b4dff872dd0 100644 --- a/packages/compass-e2e-tests/helpers/commands/index.ts +++ b/packages/compass-e2e-tests/helpers/commands/index.ts @@ -53,6 +53,7 @@ export * from './drop-database-from-sidebar'; export * from './toggle-aggregation-side-panel'; export * from './add-wizard'; export * from './set-combo-box-value'; +export * from './set-multi-combo-box-value'; export * from './wait-for-export-to-finish'; export * from './create-index'; export * from './drop-index'; diff --git a/packages/compass-e2e-tests/helpers/commands/set-multi-combo-box-value.ts b/packages/compass-e2e-tests/helpers/commands/set-multi-combo-box-value.ts new file mode 100644 index 00000000000..39b44447029 --- /dev/null +++ b/packages/compass-e2e-tests/helpers/commands/set-multi-combo-box-value.ts @@ -0,0 +1,46 @@ +import type { CompassBrowser } from '../compass-browser'; + +export async function setMultiComboBoxValue( + browser: CompassBrowser, + comboboxSelector: string, + comboboxInputSelector: string, + comboboxValues: string[] +): Promise { + await browser.$(comboboxSelector).waitForDisplayed(); + + // Clear existing values. + const existingDataTypeRemoveButtonSelector = `${comboboxSelector} [data-testid="chip-dismiss-button"]`; + + while (await browser.$(existingDataTypeRemoveButtonSelector).isDisplayed()) { + await browser.clickVisible(existingDataTypeRemoveButtonSelector); + } + + // Focus the combobox. + await browser.clickVisible(comboboxInputSelector); + const inputElement = browser.$(comboboxInputSelector); + await browser.waitUntil(async () => { + const isFocused = await inputElement.isFocused(); + if (isFocused === true) { + return true; + } else { + // Try to click again. + await inputElement.click(); + return false; + } + }); + + const controlledMenuId: string = await inputElement.getAttribute( + 'aria-controls' + ); + const comboboxListSelectorElement = browser.$( + `[id="${controlledMenuId}"][role="listbox"]` + ); + await comboboxListSelectorElement.waitForDisplayed(); + + for (const value of comboboxValues) { + await browser.setValueVisible(comboboxInputSelector, value); + await browser.keys(['Enter']); + } + await browser.keys(['Escape']); + await comboboxListSelectorElement.waitForDisplayed({ reverse: true }); +} diff --git a/packages/compass-e2e-tests/helpers/selectors.ts b/packages/compass-e2e-tests/helpers/selectors.ts index da07b9a5be4..2615a6cf244 100644 --- a/packages/compass-e2e-tests/helpers/selectors.ts +++ b/packages/compass-e2e-tests/helpers/selectors.ts @@ -1451,6 +1451,22 @@ export const DataModelPreviewCollection = (collectionId: string) => `${DataModelPreview} [data-id="${collectionId}"]`; // TODO(COMPASS-9719): add once we upgrade reactflow again in diagramming: [aria-roleDescription="node"] export const DataModelPreviewRelationship = (relationshipId: string) => `${DataModelPreview} [data-id="${relationshipId}"]`; // TODO(COMPASS-9719): add once we upgrade reactflow again in diagramming: [aria-roleDescription="edge"] +export const DataModelCollectionAddFieldBtn = (collectionId: string) => + `${DataModelPreview} [data-id="${collectionId}"] [data-testid="data-model-collection-add-field"]`; // TODO(COMPASS-9719): add once we upgrade reactflow again in diagramming: [aria-roleDescription="node"] +export const DataModelAddNestedFieldBtn = ( + collectionId: string, + fieldPath: string[] +) => + `${DataModelPreview} [data-id="${collectionId}"] [data-testid="data-model-field-type-${fieldPath.join( + '-' + )}"]`; +export const DataModelCollectionField = ( + collectionId: string, + fieldPath: string[] +) => + `${DataModelPreview} [data-id="${collectionId}"] [data-testid="selectable-field-${collectionId}-${fieldPath.join( + '.' + )}"]`; // selectable-field-test.testCollection-one-renamedField.field-2 export const DataModelApplyEditor = `${DataModelEditor} [data-testid="apply-editor"]`; export const DataModelEditorApplyButton = `${DataModelApplyEditor} [data-testid="apply-button"]`; export const DataModelUndoButton = 'button[aria-label="Undo"]'; @@ -1478,6 +1494,9 @@ export const DataModelsListItemActions = (diagramName: string) => export const DataModelsListItemDeleteButton = `[data-action="delete"]`; export const DataModelAddRelationshipBtn = 'aria/Add Relationship'; export const DataModelAddCollectionBtn = 'aria/Add Collection'; +export const DataModelFieldTypeCombobox = '[data-testid="field-type-combobox"]'; +export const DataModelFieldTypeComboboxInput = + '[data-testid="field-type-combobox"] [role="combobox"] input'; export const DataModelNameInputLabel = '//label[text()="Name"]'; export const DataModelNameInput = 'input[data-testid="data-model-collection-drawer-name-input"]'; diff --git a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts index 91e51119cfc..3df34baac72 100644 --- a/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts +++ b/packages/compass-e2e-tests/tests/data-modeling-tab.test.ts @@ -234,15 +234,110 @@ async function dragNode( y: Math.round(startPosition.y + 15), // we're aiming for the header area (top of the node) }) .down({ button: 0 }) // Left mouse button - .move({ duration: 1000, origin: 'pointer', ...pointerActionMoveParams }) - .pause(1000) - .move({ duration: 1000, origin: 'pointer', ...pointerActionMoveParams }) + .move({ duration: 500, origin: 'pointer', ...pointerActionMoveParams }) + .pause(500) + .move({ duration: 500, origin: 'pointer', ...pointerActionMoveParams }) .up({ button: 0 }) // Release the left mouse button .perform(); await browser.waitForAnimations(node); return startPosition; } +/** + * Drags the diagram view to show the specified element until it's clickable and clicks. + * This is useful when we're trying to interact with an element that may be partially outside + * of the view. Note that there isn't any check that a node is covering the space that will be + * dragged, so it can end up dragging nodes unintentionally. + **/ +async function dragDiagramToShowAndClick( + browser: CompassBrowser, + selector: string +) { + const targetElement = browser.$(selector); + + let elementPosition = await targetElement.getLocation(); + + const DRAG_INCREMENT = 100; + + let diagramBackgroundPosition = await browser + .$(Selectors.DataModelPreview) + .getLocation(); + + async function attemptClick() { + try { + // In the diagram the buttons on nodes will return true for `isClickable` and `isDisplayed` + // withinViewport even when it errors on click. So we have to attempt a click to be sure. + await targetElement.click(); + } catch { + return false; + } + + return true; + } + + let dragAndClickAttempts = 0; + + // Drag in increments as the diagram can be large, and we can't drag off of the page in one go. + while (!(await attemptClick())) { + elementPosition = await targetElement.getLocation(); + + diagramBackgroundPosition = await browser + .$(Selectors.DataModelPreview) + .getLocation(); + + // Start a bit away from the origin 5 to give space for the drag to happen. + const baseX = Math.round(diagramBackgroundPosition.x + DRAG_INCREMENT + 5); + const baseY = Math.round(diagramBackgroundPosition.y + DRAG_INCREMENT + 5); + + const moveX = + Math.abs(diagramBackgroundPosition.x - elementPosition.x) > + DRAG_INCREMENT * 2 + ? Math.round( + DRAG_INCREMENT * + (diagramBackgroundPosition.x > elementPosition.x ? 1 : -1) + ) + : 0; + const moveY = + Math.abs(diagramBackgroundPosition.y - elementPosition.y) > + DRAG_INCREMENT * 2 + ? Math.round( + DRAG_INCREMENT * + (diagramBackgroundPosition.y > elementPosition.y ? 1 : -1) + ) + : 0; + + await browser + .action('pointer') + .move({ + x: baseX, + y: baseY, + }) + .down({ button: 0 }) // Left mouse button. + .pause(250) + .move({ + duration: 250, + origin: 'pointer', + x: moveX, + y: moveY, + }) + .up({ button: 0 }) // Release the left mouse button. + .perform(); + + await browser.waitForAnimations(Selectors.DataModelPreview); + + dragAndClickAttempts--; + if (dragAndClickAttempts > 20) { + throw new Error( + `Could not drag the diagram to show and click the element with selector ${selector}. Attempted to reposition and click ${dragAndClickAttempts} times.` + ); + } + } + + await browser.waitForAnimations(Selectors.DataModelPreview); + + return elementPosition; +} + describe('Data Modeling tab', function () { let compass: Compass; let browser: CompassBrowser; @@ -834,5 +929,107 @@ describe('Data Modeling tab', function () { await browser.clickVisible(Selectors.DataModelUndoButton); await getDiagramNodes(browser, 2); }); + + it('allows field editing', async function () { + const dataModelName = 'Test Edit Collection'; + await setupDiagram(browser, { + diagramName: dataModelName, + connectionName: DEFAULT_CONNECTION_NAME_1, + databaseName: 'test', + }); + + const dataModelEditor = browser.$(Selectors.DataModelEditor); + await dataModelEditor.waitForDisplayed(); + + let collectionText = await browser + .$(Selectors.DataModelPreviewCollection('test.testCollection-one')) + .getText(); + expect(collectionText).to.not.include('field-1'); + + // Drag the node to show add new field buttons and new fields. + await dragNode( + browser, + Selectors.DataModelPreviewCollection('test.testCollection-one'), + { x: -100, y: 0 } + ); + + // Add two fields to the collection. + await dragDiagramToShowAndClick( + browser, + Selectors.DataModelCollectionAddFieldBtn('test.testCollection-one') + ); + await dragDiagramToShowAndClick( + browser, + Selectors.DataModelCollectionAddFieldBtn('test.testCollection-one') + ); + + // Verify they both exist. + await browser.waitUntil(async () => { + collectionText = await browser + .$(Selectors.DataModelPreviewCollection('test.testCollection-one')) + .getText(); + return ( + collectionText.includes('field-1') && + collectionText.includes('field-2') + ); + }); + + // Rename the field (field-2 is selected). + await browser.setValueVisible( + browser.$(Selectors.DataModelNameInput), + 'renamedField' + ); + await browser.$(Selectors.SideDrawer).click(); // Unfocus the input. + + // Ensure the name is updated in the diagram. + await browser.waitUntil(async () => { + collectionText = await browser + .$(Selectors.DataModelPreviewCollection('test.testCollection-one')) + .getText(); + const previousNameExists = collectionText.includes('field-2'); + const renamedFieldExists = collectionText.includes('renamedField'); + + return !previousNameExists && renamedFieldExists; + }); + + // Change the field type to object. + await browser.setMultiComboBoxValue( + Selectors.DataModelFieldTypeCombobox, + Selectors.DataModelFieldTypeComboboxInput, + ['object'] + ); + await browser.$(Selectors.SideDrawer).click(); // Unfocus the input. + + await dragDiagramToShowAndClick( + browser, + Selectors.DataModelAddNestedFieldBtn('test.testCollection-one', [ + 'renamedField', + ]) + ); + await dragDiagramToShowAndClick( + browser, + Selectors.DataModelAddNestedFieldBtn('test.testCollection-one', [ + 'renamedField', + ]) + ); + + // Drag the node to show add new field buttons and new fields. + await dragNode( + browser, + Selectors.DataModelPreviewCollection('test.testCollection-one'), + { x: 0, y: -100 } + ); + + // Ensure the new fields are in the diagram. + const newFieldText = await browser + .$( + Selectors.DataModelCollectionField('test.testCollection-one', [ + 'renamedField', + 'field-2', + ]) + ) + .getText(); + expect(newFieldText).to.include('field-2'); + }); }); });