diff --git a/packages/compass-e2e-tests/tests/instance-my-queries-tab.test.ts b/packages/compass-e2e-tests/tests/instance-my-queries-tab.test.ts index 15a5a5850fa..d0752c41e1c 100644 --- a/packages/compass-e2e-tests/tests/instance-my-queries-tab.test.ts +++ b/packages/compass-e2e-tests/tests/instance-my-queries-tab.test.ts @@ -245,4 +245,117 @@ describe('Instance my queries tab', function () { const namespace = await browser.getActiveTabNamespace(); expect(namespace).to.equal('test.numbers'); }); + + context( + 'when a user has a saved query associated with a collection that does not exist', + function () { + const favoriteQueryName = 'list of numbers greater than 10 - query'; + const newCollectionName = 'numbers-renamed'; + + /** saves a query and renames the collection associated with the query, so that the query must be opened with the "select namespace" modal */ + async function setup() { + // Run a query + await browser.navigateToCollectionTab('test', 'numbers', 'Documents'); + await browser.runFindOperation('Documents', `{i: {$gt: 10}}`, { + limit: '10', + }); + await browser.clickVisible(Selectors.QueryBarHistoryButton); + + // Wait for the popover to show + const history = await browser.$(Selectors.QueryBarHistory); + await history.waitForDisplayed(); + + // wait for the recent item to show. + const recentCard = await browser.$(Selectors.QueryHistoryRecentItem); + await recentCard.waitForDisplayed(); + + // Save the ran query + await browser.hover(Selectors.QueryHistoryRecentItem); + await browser.clickVisible(Selectors.QueryHistoryFavoriteAnItemButton); + await browser.setValueVisible( + Selectors.QueryHistoryFavoriteItemNameField, + favoriteQueryName + ); + await browser.clickVisible( + Selectors.QueryHistorySaveFavoriteItemButton + ); + + await browser.closeWorkspaceTabs(); + await browser.navigateToInstanceTab('Databases'); + await browser.navigateToInstanceTab('My Queries'); + + // open the menu + await openMenuForQueryItem(browser, favoriteQueryName); + + // copy to clipboard + await browser.clickVisible(Selectors.SavedItemMenuItemCopy); + + if (process.env.COMPASS_E2E_DISABLE_CLIPBOARD_USAGE !== 'true') { + await browser.waitUntil( + async () => { + const text = (await clipboard.read()) + .replace(/\s+/g, ' ') + .replace(/\n/g, ''); + const isValid = + text === + '{ "collation": null, "filter": { "i": { "$gt": 10 } }, "limit": 10, "project": null, "skip": null, "sort": null }'; + if (!isValid) { + console.log(text); + } + return isValid; + }, + { timeoutMsg: 'Expected copy to clipboard to work' } + ); + } + + // rename the collection associated with the query to force the open item modal + await browser.shellEval('use test'); + await browser.shellEval( + `db.numbers.renameCollection('${newCollectionName}')` + ); + await browser.clickVisible(Selectors.SidebarRefreshDatabasesButton); + } + beforeEach(setup); + + it('users can permanently associate a new namespace for an aggregation/query', async function () { + await browser.navigateToInstanceTab('My Queries'); + // browse to the query + await browser.clickVisible(Selectors.myQueriesItem(favoriteQueryName)); + + // the open item modal - select a new collection + const openModal = await browser.$(Selectors.OpenSavedItemModal); + await openModal.waitForDisplayed(); + await browser.selectOption( + Selectors.OpenSavedItemDatabaseField, + 'test' + ); + await browser.selectOption( + Selectors.OpenSavedItemCollectionField, + newCollectionName + ); + + await browser.clickParent( + '[data-testid="update-query-aggregation-checkbox"]' + ); + + const confirmOpenButton = await browser.$( + Selectors.OpenSavedItemModalConfirmButton + ); + await confirmOpenButton.waitForEnabled(); + + await confirmOpenButton.click(); + await openModal.waitForDisplayed({ reverse: true }); + + await browser.navigateToInstanceTab('My Queries'); + + const [databaseNameElement, collectionNameElement] = [ + await browser.$('span=test'), + await browser.$(`span=${newCollectionName}`), + ]; + + await databaseNameElement.waitForDisplayed(); + await collectionNameElement.waitForDisplayed(); + }); + } + ); }); diff --git a/packages/compass-saved-aggregations-queries/src/components/open-item-modal.tsx b/packages/compass-saved-aggregations-queries/src/components/open-item-modal.tsx index e85188c4427..f250fae5e2f 100644 --- a/packages/compass-saved-aggregations-queries/src/components/open-item-modal.tsx +++ b/packages/compass-saved-aggregations-queries/src/components/open-item-modal.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { + Checkbox, FormModal, Option, Select, @@ -14,6 +15,7 @@ import { openSelectedItem, selectCollection, selectDatabase, + updateItemNamespaceChecked, } from '../stores/open-item'; type AsyncItemsSelectProps = { @@ -98,8 +100,10 @@ type OpenItemModalProps = { itemName: string; isModalOpen: boolean; isSubmitDisabled: boolean; + updateItemNamespace: boolean; onSubmit(): void; onClose(): void; + onUpdateNamespaceChecked(checked: boolean): void; }; const modalContent = css({ @@ -107,6 +111,7 @@ const modalContent = css({ gridTemplateAreas: ` 'description description' 'database collection' + 'checkbox checkbox' `, gridAutoColumns: '1fr', rowGap: spacing[4], @@ -125,20 +130,26 @@ const collectionSelect = css({ gridArea: 'collection', }); +const checkbox = css({ + gridArea: 'checkbox', +}); + const OpenItemModal: React.FunctionComponent = ({ namespace, itemType, itemName, isModalOpen, isSubmitDisabled, + updateItemNamespace, onClose, onSubmit, + onUpdateNamespaceChecked, }) => { return ( onSubmit()} title="Select a Namespace" submitButtonText="Open" submitDisabled={isSubmitDisabled} @@ -161,6 +172,15 @@ const OpenItemModal: React.FunctionComponent = ({ label="Collection" > + { + onUpdateNamespaceChecked(event.target.checked); + }} + label={`Update this ${itemType} with the newly selected namespace`} + data-testid="update-query-aggregation-checkbox" + /> ); @@ -169,7 +189,12 @@ const OpenItemModal: React.FunctionComponent = ({ const mapState: MapStateToProps< Pick< OpenItemModalProps, - 'isModalOpen' | 'isSubmitDisabled' | 'namespace' | 'itemType' | 'itemName' + | 'isModalOpen' + | 'isSubmitDisabled' + | 'namespace' + | 'itemType' + | 'itemName' + | 'updateItemNamespace' >, Record, RootState @@ -179,6 +204,7 @@ const mapState: MapStateToProps< selectedDatabase, selectedCollection, selectedItem: item, + updateItemNamespace, }, }) => { return { @@ -187,15 +213,17 @@ const mapState: MapStateToProps< namespace: `${item?.database ?? ''}.${item?.collection ?? ''}`, itemName: item?.name ?? '', itemType: item?.type ?? '', + updateItemNamespace, }; }; const mapDispatch: MapDispatchToProps< - Pick, + Pick, Record > = { onSubmit: openSelectedItem, onClose: closeModal, + onUpdateNamespaceChecked: updateItemNamespaceChecked, }; export default connect(mapState, mapDispatch)(OpenItemModal); diff --git a/packages/compass-saved-aggregations-queries/src/stores/open-item.ts b/packages/compass-saved-aggregations-queries/src/stores/open-item.ts index 6d1256ec935..9a9614a38bb 100644 --- a/packages/compass-saved-aggregations-queries/src/stores/open-item.ts +++ b/packages/compass-saved-aggregations-queries/src/stores/open-item.ts @@ -14,6 +14,7 @@ export type State = { collections: string[]; selectedCollection: string | null; collectionsStatus: Status; + updateItemNamespace: boolean; }; const INITIAL_STATE: State = { @@ -26,6 +27,7 @@ const INITIAL_STATE: State = { collections: [], selectedCollection: null, collectionsStatus: 'initial', + updateItemNamespace: false, }; export enum ActionTypes { @@ -40,6 +42,7 @@ export enum ActionTypes { LoadCollections = 'compass-saved-aggregations-queries/loadCollections', LoadCollectionsSuccess = 'compass-saved-aggregations-queries/loadCollectionsSuccess', LoadCollectionsError = 'compass-saved-aggregations-queries/loadCollectionsError', + UpdateNamespaceChecked = 'compass-saved-aggregations-queries/updateNamespaceChecked', } type OpenModalAction = { @@ -92,6 +95,11 @@ type LoadCollectionsErrorAction = { type: ActionTypes.LoadCollectionsError; }; +type UpdateNamespaceChecked = { + type: ActionTypes.UpdateNamespaceChecked; + updateItemNamespace: boolean; +}; + export type Actions = | OpenModalAction | CloseModalAction @@ -103,7 +111,8 @@ export type Actions = | SelectCollectionAction | LoadCollectionsAction | LoadCollectionsErrorAction - | LoadCollectionsSuccessAction; + | LoadCollectionsSuccessAction + | UpdateNamespaceChecked; const reducer: Reducer = (state = INITIAL_STATE, action) => { switch (action.type) { @@ -165,11 +174,21 @@ const reducer: Reducer = (state = INITIAL_STATE, action) => { collections: action.collections, collectionsStatus: 'ready', }; + case ActionTypes.UpdateNamespaceChecked: + return { + ...state, + updateItemNamespace: action.updateItemNamespace, + }; default: return state; } }; +export const updateItemNamespaceChecked = (updateItemNamespace: boolean) => ({ + type: ActionTypes.UpdateNamespaceChecked, + updateItemNamespace, +}); + const openModal = (selectedItem: Item): SavedQueryAggregationThunkAction> => async (dispatch, _getState, { instance, dataService }) => { @@ -249,16 +268,44 @@ export const openSavedItem = dispatch(openItem(item, database, collection)); }; +export const updateNamespaceChecked = + (updateNamespaceChecked: boolean): SavedQueryAggregationThunkAction => + (dispatch) => { + dispatch({ + type: ActionTypes.UpdateNamespaceChecked, + updateNamespaceChecked, + }); + }; + export const openSelectedItem = - (): SavedQueryAggregationThunkAction => (dispatch, getState) => { + (): SavedQueryAggregationThunkAction> => + async (dispatch, getState, { queryStorage, pipelineStorage }) => { const { - openItem: { selectedItem, selectedDatabase, selectedCollection }, + openItem: { + selectedItem, + selectedDatabase, + selectedCollection, + updateItemNamespace, + }, } = getState(); if (!selectedItem || !selectedDatabase || !selectedCollection) { return; } + if (updateItemNamespace) { + const id = selectedItem.id; + const newNamespace = `${selectedDatabase}.${selectedCollection}`; + + if (selectedItem.type === 'aggregation') { + await pipelineStorage?.updateAttributes(id, { + namespace: newNamespace, + }); + } else if (selectedItem.type === 'query') { + await queryStorage?.updateAttributes(id, { _ns: newNamespace }); + } + } + dispatch({ type: ActionTypes.CloseModal }); dispatch(openItem(selectedItem, selectedDatabase, selectedCollection)); };