diff --git a/packages/compass-components/src/components/modals/modal.tsx b/packages/compass-components/src/components/modals/modal.tsx index f7bc7502a71..8f20756295f 100644 --- a/packages/compass-components/src/components/modals/modal.tsx +++ b/packages/compass-components/src/components/modals/modal.tsx @@ -8,6 +8,10 @@ const contentStyles = css({ width: '600px', letterSpacing: 0, padding: 0, + // The LG modal applies transform: translate3d(0, 0, 0) style to the modal + // content and this messes up the autocompleter within the modal. So we clear + // the transform here. + transform: 'none', }); const modalFullScreenStyles = css({ diff --git a/packages/compass-editor/src/codemirror/search-index-autocompleter.test.ts b/packages/compass-editor/src/codemirror/search-index-autocompleter.test.ts new file mode 100644 index 00000000000..21f48cc0539 --- /dev/null +++ b/packages/compass-editor/src/codemirror/search-index-autocompleter.test.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import { createSearchIndexAutocompleter } from './search-index-autocompleter'; +import { setupCodemirrorCompleter } from '../../test/completer'; + +describe('search-index autocompleter', function () { + const { getCompletions, cleanup } = setupCodemirrorCompleter( + createSearchIndexAutocompleter + ); + + after(cleanup); + + it('returns words in context when its not completing fields', function () { + const completions = getCompletions('{ dynamic: true, type: "dy', { + fields: ['_id', 'name', 'age'], + }); + expect(completions.map((x) => x.label)).to.deep.equal([ + 'dynamic', + 'true', + 'type', + ]); + }); + + it('returns field names when autocompleting fields', function () { + const completions = getCompletions('{ fields: { "a', { + fields: ['_id', 'name', 'age'], + }); + expect(completions.map((x) => x.label)).to.deep.equal([ + '_id', + 'name', + 'age', + ]); + }); +}); diff --git a/packages/compass-editor/src/codemirror/search-index-autocompleter.ts b/packages/compass-editor/src/codemirror/search-index-autocompleter.ts new file mode 100644 index 00000000000..fed92a1c74b --- /dev/null +++ b/packages/compass-editor/src/codemirror/search-index-autocompleter.ts @@ -0,0 +1,45 @@ +import type { CompletionSource } from '@codemirror/autocomplete'; +import type { CompletionOptions } from '../autocompleter'; +import { completer } from '../autocompleter'; +import { + ID_REGEX, + createCompletionResultForIdPrefix, +} from './ace-compat-autocompleter'; +import { + completeWordsInString, + getAncestryOfToken, + resolveTokenAtCursor, +} from './utils'; + +const isCompletingFields = (ancestors: string[]) => { + return ancestors[ancestors.length - 1] === 'fields'; +}; + +export const createSearchIndexAutocompleter = ( + options: Pick = {} +): CompletionSource => { + const completions = completer('', { + meta: ['field:identifier'], + ...options, + }); + + return (context) => { + const token = resolveTokenAtCursor(context); + const document = context.state.sliceDoc(0); + const prefix = context.matchBefore(ID_REGEX); + if (!prefix) { + return null; + } + + const ancestors = getAncestryOfToken(token, document); + + if (isCompletingFields(ancestors)) { + return createCompletionResultForIdPrefix({ + prefix, + completions, + }); + } + + return completeWordsInString(context); + }; +}; diff --git a/packages/compass-editor/src/index.ts b/packages/compass-editor/src/index.ts index 2d567654a6a..dae27f85d78 100644 --- a/packages/compass-editor/src/index.ts +++ b/packages/compass-editor/src/index.ts @@ -20,3 +20,4 @@ export { createValidationAutocompleter } from './codemirror/validation-autocompl export { createQueryAutocompleter } from './codemirror/query-autocompleter'; export { createStageAutocompleter } from './codemirror/stage-autocompleter'; export { createAggregationAutocompleter } from './codemirror/aggregation-autocompleter'; +export { createSearchIndexAutocompleter } from './codemirror/search-index-autocompleter'; diff --git a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx index 7e6e4569cfe..2212bdc4c47 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.spec.tsx @@ -38,6 +38,7 @@ describe('Create Search Index Modal', function () { onSubmit={onSubmitSpy} onClose={onCloseSpy} error={'Invalid index definition.'} + fields={[]} /> ); }); diff --git a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx index f583a605acd..8283de3c730 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/base-search-index-modal.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { + useCallback, + useEffect, + useRef, + useState, + useMemo, +} from 'react'; import { Modal, ModalFooter, @@ -18,13 +24,17 @@ import { Banner, rafraf, } from '@mongodb-js/compass-components'; -import { CodemirrorMultilineEditor } from '@mongodb-js/compass-editor'; +import { + CodemirrorMultilineEditor, + createSearchIndexAutocompleter, +} from '@mongodb-js/compass-editor'; import type { EditorRef } from '@mongodb-js/compass-editor'; import _parseShellBSON, { ParseMode } from 'ejson-shell-parser'; import type { Document } from 'mongodb'; import { useTrackOnChange } from '@mongodb-js/compass-logging'; import { SearchIndexTemplateDropdown } from '../search-index-template-dropdown'; import type { SearchTemplate } from '@mongodb-js/mongodb-constants'; +import type { Field } from '../../modules/fields'; // Copied from packages/compass-aggregations/src/modules/pipeline-builder/pipeline-parser/utils.ts function parseShellBSON(source: string): Document[] { @@ -82,6 +92,7 @@ type BaseSearchIndexModalProps = { isModalOpen: boolean; isBusy: boolean; error: string | undefined; + fields: Field[]; onSubmit: (indexName: string, indexDefinition: Document) => void; onClose: () => void; }; @@ -95,6 +106,7 @@ export const BaseSearchIndexModal: React.FunctionComponent< isModalOpen, isBusy, error, + fields, onSubmit, onClose, }) => { @@ -171,6 +183,14 @@ export const BaseSearchIndexModal: React.FunctionComponent< [editorRef] ); + const completer = useMemo( + () => + createSearchIndexAutocompleter({ + fields, + }), + [fields] + ); + return ( {parsingError && } diff --git a/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx index 328f832918a..56eba3d0ebe 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/create-search-index-modal.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import type { RootState } from '../../modules'; import type { Document } from 'mongodb'; import { BaseSearchIndexModal } from './base-search-index-modal'; +import type { Field } from '../../modules/fields'; export const DEFAULT_INDEX_DEFINITION = `{ mappings: { @@ -15,13 +16,14 @@ type CreateSearchIndexModalProps = { isModalOpen: boolean; isBusy: boolean; error: string | undefined; + fields: Field[]; onCreateIndex: (indexName: string, indexDefinition: Document) => void; onCloseModal: () => void; }; export const CreateSearchIndexModal: React.FunctionComponent< CreateSearchIndexModalProps -> = ({ isModalOpen, isBusy, error, onCreateIndex, onCloseModal }) => { +> = ({ isModalOpen, isBusy, error, fields, onCreateIndex, onCloseModal }) => { return ( @@ -40,10 +43,12 @@ const mapState = ({ searchIndexes: { createIndex: { isBusy, isModalOpen, error }, }, + fields, }: RootState) => ({ isModalOpen, isBusy, error, + fields, }); const mapDispatch = { diff --git a/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx b/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx index 56781961ae5..8b0cb10996b 100644 --- a/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx +++ b/packages/compass-indexes/src/components/search-indexes-modals/update-search-index-modal.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import type { RootState } from '../../modules'; import type { Document } from 'mongodb'; import { BaseSearchIndexModal } from './base-search-index-modal'; +import type { Field } from '../../modules/fields'; type UpdateSearchIndexModalProps = { indexName: string; @@ -11,6 +12,7 @@ type UpdateSearchIndexModalProps = { isModalOpen: boolean; isBusy: boolean; error: string | undefined; + fields: Field[]; onUpdateIndex: (indexName: string, indexDefinition: Document) => void; onCloseModal: () => void; }; @@ -23,6 +25,7 @@ export const UpdateSearchIndexModal: React.FunctionComponent< isModalOpen, isBusy, error, + fields, onUpdateIndex, onCloseModal, }) => { @@ -34,6 +37,7 @@ export const UpdateSearchIndexModal: React.FunctionComponent< isModalOpen={isModalOpen} isBusy={isBusy} error={error} + fields={fields} onSubmit={onUpdateIndex} onClose={onCloseModal} /> @@ -45,6 +49,7 @@ const mapState = ({ indexes, updateIndex: { indexName, isBusy, isModalOpen, error }, }, + fields, }: RootState) => ({ isModalOpen, isBusy, @@ -55,6 +60,7 @@ const mapState = ({ 2 ), error, + fields, }); const mapDispatch = { diff --git a/packages/compass-indexes/src/modules/fields.ts b/packages/compass-indexes/src/modules/fields.ts new file mode 100644 index 00000000000..2437c7e50c1 --- /dev/null +++ b/packages/compass-indexes/src/modules/fields.ts @@ -0,0 +1,32 @@ +import type { AnyAction } from 'redux'; +import { isAction } from './../utils/is-action'; + +export enum ActionTypes { + SetFields = 'indexes/SetFields', +} + +export type Field = { + name: string; + description: string; +}; + +type SetFieldsAction = { + type: ActionTypes.SetFields; + fields: Field[]; +}; + +type State = Field[]; + +export const INITIAL_STATE: State = []; + +export default function reducer(state = INITIAL_STATE, action: AnyAction) { + if (isAction(action, ActionTypes.SetFields)) { + return action.fields; + } + return state; +} + +export const setFields = (fields: Field[]): SetFieldsAction => ({ + type: ActionTypes.SetFields, + fields, +}); diff --git a/packages/compass-indexes/src/modules/index.ts b/packages/compass-indexes/src/modules/index.ts index bc211c155b7..30846d82c58 100644 --- a/packages/compass-indexes/src/modules/index.ts +++ b/packages/compass-indexes/src/modules/index.ts @@ -10,6 +10,7 @@ import regularIndexes from './regular-indexes'; import searchIndexes from './search-indexes'; import serverVersion from './server-version'; import namespace from './namespace'; +import fields from './fields'; import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; const reducer = combineReducers({ @@ -22,6 +23,7 @@ const reducer = combineReducers({ namespace, regularIndexes, searchIndexes, + fields, }); export type SortDirection = 'asc' | 'desc'; diff --git a/packages/compass-indexes/src/stores/store.ts b/packages/compass-indexes/src/stores/store.ts index 207d00d17a9..3480a3e9309 100644 --- a/packages/compass-indexes/src/stores/store.ts +++ b/packages/compass-indexes/src/stores/store.ts @@ -22,6 +22,7 @@ import { } from '../modules/search-indexes'; import type { DataService } from 'mongodb-data-service'; import type AppRegistry from 'hadron-app-registry'; +import { setFields } from '../modules/fields'; import { switchToRegularIndexes } from '../modules/index-view'; export type IndexesDataService = Pick< @@ -62,6 +63,7 @@ const configureStore = (options: ConfigureStoreOptions) => { namespace: options.namespace, serverVersion: options.serverVersion, isReadonlyView: options.isReadonly, + fields: [], indexView: INDEX_LIST_INITIAL_STATE, searchIndexes: { ...SEARCH_INDEXES_INITIAL_STATE, @@ -105,6 +107,10 @@ const configureStore = (options: ConfigureStoreOptions) => { store.dispatch(inProgressIndexFailed(data)); } ); + + localAppRegistry.on('fields-changed', (fields) => { + store.dispatch(setFields(fields.autocompleteFields)); + }); } if (options.globalAppRegistry) {