diff --git a/packages/compass-editor/src/codemirror/query-autocompleter-with-history.ts b/packages/compass-editor/src/codemirror/query-autocompleter-with-history.ts index e4b6b07bf1a..10d86715a55 100644 --- a/packages/compass-editor/src/codemirror/query-autocompleter-with-history.ts +++ b/packages/compass-editor/src/codemirror/query-autocompleter-with-history.ts @@ -18,17 +18,20 @@ export const createQueryWithHistoryAutocompleter = ({ options = {}, queryProperty, onApply, + onDelete, theme, }: { savedQueries: SavedQuery[]; options?: Pick; queryProperty: string; onApply: (query: SavedQuery['queryProperties']) => void; + onDelete: (queryId: string, type: 'recent' | 'favorite') => void; theme: CodemirrorThemeType; }): CompletionSource => { const queryHistoryAutocompleter = createQueryHistoryAutocompleter({ savedQueries, onApply, + onDelete, queryProperty, theme, }); diff --git a/packages/compass-editor/src/codemirror/query-history-autocompleter.ts b/packages/compass-editor/src/codemirror/query-history-autocompleter.ts index c617d9251c5..d6c50d6f900 100644 --- a/packages/compass-editor/src/codemirror/query-history-autocompleter.ts +++ b/packages/compass-editor/src/codemirror/query-history-autocompleter.ts @@ -14,7 +14,8 @@ import { highlightCode } from '@lezer/highlight'; import { type CodemirrorThemeType, highlightStyles } from '../editor'; export type SavedQuery = { - type: string; + id: string; + type: 'recent' | 'favorite'; lastExecuted: Date; queryProperties: { [propertyName: string]: any; @@ -103,10 +104,12 @@ function getMatchingQueryHistoryItemsForInput({ export const createQueryHistoryAutocompleter = ({ savedQueries, onApply, + onDelete, queryProperty, theme, }: { savedQueries: SavedQuery[]; + onDelete: (queryId: string, type: 'recent' | 'favorite') => void; onApply: (query: SavedQuery['queryProperties']) => void; queryProperty: string; theme: CodemirrorThemeType; @@ -134,7 +137,12 @@ export const createQueryHistoryAutocompleter = ({ label: createQueryLabel(query, queryProperty), type: query.type === 'recent' ? 'query-history' : 'favorite', detail: formatDate(query.lastExecuted.getTime()), - info: () => createInfo(query, theme).dom, + info: () => + createInfo({ + query, + theme, + onDelete: () => onDelete(query.id, query.type), + }), apply: () => { onApply(query.queryProperties); }, @@ -155,20 +163,44 @@ export const createQueryHistoryAutocompleter = ({ }; }; +const queryInfoContainerStyles = css({ + minWidth: spacing[800] * 3, +}); + const queryLabelStyles = css({ textTransform: 'capitalize', fontWeight: 'bold', fontFamily: fontFamilies.default, + marginTop: `${spacing[100]}px`, }); const queryCodeStyles = css({ fontFamily: fontFamilies.code, margin: `${spacing[50]}px`, + marginTop: `${spacing[100]}px`, marginLeft: `${spacing[100]}px`, padding: 0, whiteSpace: 'pre-wrap', }); +const removeButtonStyles = css({ + background: 'none', + border: 'none', + color: 'inherit', + font: 'inherit', + cursor: 'pointer', + padding: 0, + textDecoration: 'underline', + fontSize: 'inherit', + fontFamily: fontFamilies.default, + position: 'absolute', + top: `${spacing[50]}px`, + right: `${spacing[100]}px`, + '&:hover': { + cursor: 'pointer', + }, +}); + export function createQueryLabel( query: SavedQuery, propertyName: string @@ -195,21 +227,52 @@ export function createQueryDisplayLabel(query: SavedQuery): string { .reduce((acc, curr) => (acc ? `${acc}, ${curr}` : curr), ''); } +function createRemoveButton({ + onDelete, +}: { + onDelete: () => void; +}): HTMLButtonElement { + const button = document.createElement('button'); + + button.textContent = 'Remove'; + button.className = removeButtonStyles; + button.onclick = () => { + onDelete(); + }; + button.type = 'button'; + button.setAttribute('aria-label', 'Remove query from history'); + button.setAttribute('data-testid', 'remove-query-history-item'); + + return button; +} + const javascriptExpressionLanguageParser = languages['javascript-expression']().language.parser; -function createInfo( - query: SavedQuery, - theme: CodemirrorThemeType -): { +function createInfo({ + query, + onDelete, + theme, +}: { + query: SavedQuery; + onDelete: () => void; + theme: CodemirrorThemeType; +}): { dom: Node; destroy?: () => void; } { const customHighlighter = highlightStyles[theme]; const container = document.createElement('div'); + container.className = queryInfoContainerStyles; Object.entries(query.queryProperties).forEach(([key, value]) => { const formattedQuery = toJSString(value); + + const removeButton = createRemoveButton({ + onDelete, + }); + container.appendChild(removeButton); + const codeDiv = document.createElement('div'); const label = document.createElement('label'); @@ -256,6 +319,7 @@ function createInfo( while (container.firstChild) { container.removeChild(container.firstChild); } + container.remove(); }, }; } diff --git a/packages/compass-query-bar/src/components/option-editor.tsx b/packages/compass-query-bar/src/components/option-editor.tsx index 0d6850f9568..a5a1c873907 100644 --- a/packages/compass-query-bar/src/components/option-editor.tsx +++ b/packages/compass-query-bar/src/components/option-editor.tsx @@ -23,7 +23,11 @@ import { usePreference } from 'compass-preferences-model/provider'; import { lenientlyFixQuery } from '../query/leniently-fix-query'; import type { RootState } from '../stores/query-bar-store'; import { useAutocompleteFields } from '@mongodb-js/compass-field-store'; -import { applyFromHistory } from '../stores/query-bar-reducer'; +import { + applyFromHistory, + deleteFavoriteQuery, + deleteRecentQuery, +} from '../stores/query-bar-reducer'; import { getQueryAttributes } from '../utils'; import type { BaseQuery, @@ -38,6 +42,7 @@ import type { RecentQuery, } from '@mongodb-js/my-queries-storage'; import type { QueryOptionOfTypeDocument } from '../constants/query-option-definition'; +import { closeCompletion } from '@codemirror/autocomplete'; type AutoCompleteQuery = Partial & Pick; @@ -76,6 +81,48 @@ const editorWithErrorStyles = css({ }, }); +export function getOptionBasedQueries( + optionName: QueryOptionOfTypeDocument, + type: 'recent' | 'favorite', + queries: (AutoCompleteRecentQuery | AutoCompleteFavoriteQuery)[] +) { + return ( + queries + .map((query) => ({ + id: query._id as string, + type, + lastExecuted: query._lastExecuted, + // For query that's being autocompleted from the main `filter`, we want to + // show whole query to the user, so that when its applied, it will replace + // the whole query (filter, project, sort etc). + // For other options, we only want to show the query for that specific option. + queryProperties: getQueryAttributes( + optionName !== 'filter' ? { [optionName]: query[optionName] } : query + ), + })) + // Filter the query if: + // - its empty + // - its an `update` query + // - its a duplicate + .filter((query, i, arr) => { + const queryIsUpdate = 'update' in query.queryProperties; + const queryIsEmpty = Object.keys(query.queryProperties).length === 0; + if (queryIsEmpty || queryIsUpdate) { + return false; + } + return ( + i === + arr.findIndex( + (t) => + JSON.stringify(t.queryProperties) === + JSON.stringify(query.queryProperties) + ) + ); + }) + .sort((a, b) => a.lastExecuted.getTime() - b.lastExecuted.getTime()) + ); +} + type OptionEditorProps = { optionName: QueryOptionOfTypeDocument; namespace: string; @@ -97,6 +144,8 @@ type OptionEditorProps = { recentQueries: AutoCompleteRecentQuery[]; favoriteQueries: AutoCompleteFavoriteQuery[]; onApplyQuery: (query: BaseQuery, fieldsToPreserve: QueryProperty[]) => void; + onDeleteRecentQuery: (id: string) => void; + onDeleteFavoriteQuery: (id: string) => void; }; export const OptionEditor: React.FunctionComponent = ({ @@ -116,6 +165,8 @@ export const OptionEditor: React.FunctionComponent = ({ recentQueries, favoriteQueries, onApplyQuery, + onDeleteRecentQuery, + onDeleteFavoriteQuery, }) => { const editorContainerRef = useRef(null); const editorRef = useRef(null); @@ -166,6 +217,13 @@ export const OptionEditor: React.FunctionComponent = ({ fields: schemaFields, serverVersion, }, + onDelete: (id: string, type: 'recent' | 'favorite') => { + if (type === 'recent') { + onDeleteRecentQuery(id); + } else { + onDeleteFavoriteQuery(id); + } + }, onApply: (query: SavedQuery['queryProperties']) => { // When we are applying a query from `filter` field, we want to apply the whole query, // otherwise we want to preserve the other fields that are already in the current query. @@ -203,6 +261,8 @@ export const OptionEditor: React.FunctionComponent = ({ schemaFields, serverVersion, onApplyQuery, + onDeleteRecentQuery, + onDeleteFavoriteQuery, isQueryHistoryAutocompleteEnabled, darkMode, optionName, @@ -274,47 +334,6 @@ export const OptionEditor: React.FunctionComponent = ({ ); }; -export function getOptionBasedQueries( - optionName: QueryOptionOfTypeDocument, - type: 'recent' | 'favorite', - queries: (AutoCompleteRecentQuery | AutoCompleteFavoriteQuery)[] -) { - return ( - queries - .map((query) => ({ - type, - lastExecuted: query._lastExecuted, - // For query that's being autocompeted from the main `filter`, we want to - // show whole query to the user, so that when its applied, it will replace - // the whole query (filter, project, sort etc). - // For other options, we only want to show the query for that specific option. - queryProperties: getQueryAttributes( - optionName !== 'filter' ? { [optionName]: query[optionName] } : query - ), - })) - // Filter the query if: - // - its empty - // - its an `update` query - // - its a duplicate - .filter((query, i, arr) => { - const queryIsUpdate = 'update' in query.queryProperties; - const queryIsEmpty = Object.keys(query.queryProperties).length === 0; - if (queryIsEmpty || queryIsUpdate) { - return false; - } - return ( - i === - arr.findIndex( - (t) => - JSON.stringify(t.queryProperties) === - JSON.stringify(query.queryProperties) - ) - ); - }) - .sort((a, b) => a.lastExecuted.getTime() - b.lastExecuted.getTime()) - ); -} - const mapStateToProps = ({ queryBar: { namespace, serverVersion, recentQueries, favoriteQueries }, }: RootState) => ({ @@ -326,6 +345,8 @@ const mapStateToProps = ({ const mapDispatchToProps = { onApplyQuery: applyFromHistory, + onDeleteRecentQuery: deleteRecentQuery, + onDeleteFavoriteQuery: deleteFavoriteQuery, }; export default connect(mapStateToProps, mapDispatchToProps)(OptionEditor);