From 3b8d44d8a9046928e09b4d60882722d3e38f02f9 Mon Sep 17 00:00:00 2001 From: Gimir Date: Mon, 16 Dec 2024 16:05:05 +0300 Subject: [PATCH] fix(chat): fix temporary folder add/update flow (Issue #2803) (#2819) Co-authored-by: Magomed-Elbi Dzhukalaev Co-authored-by: Irina_Kartun --- .../publishConversationToOrganisation.test.ts | 45 +++++++++---------- .../publishFolderWithConversation.test.ts | 6 ++- .../src/tests/selectUploadFolder.test.ts | 2 +- .../src/ui/webElements/selectFolderModal.ts | 6 ++- .../src/components/Chat/ChangePathDialog.tsx | 18 ++++++-- .../conversations/conversations.reducers.ts | 45 ++++++------------- .../src/store/prompts/prompts.reducers.ts | 45 ++++++------------- apps/chat/src/utils/app/folders.ts | 37 ++++++++++++++- 8 files changed, 107 insertions(+), 97 deletions(-) diff --git a/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts b/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts index bef7a70672..7f2dbc42a9 100644 --- a/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts +++ b/apps/chat-e2e/src/tests/publishConversationToOrganisation.test.ts @@ -258,7 +258,8 @@ dialAdminTest( const requestName = GeneratorUtil.randomPublicationRequestName(); const newFolderName = GeneratorUtil.randomString(maxNameLength * 1.5); const cutNewFolderName = newFolderName.substring(0, maxNameLength); - const publicationPath = `${PublishPath.Organization}/${cutNewFolderName}/${ExpectedConstants.newFolderWithIndexTitle(1)}/${ExpectedConstants.newFolderWithIndexTitle(2)}/${ExpectedConstants.newFolderWithIndexTitle(3)}`; + const defaultFolderName = ExpectedConstants.newFolderWithIndexTitle(1); + const publicationPath = `${PublishPath.Organization}/${cutNewFolderName}/${defaultFolderName}/${defaultFolderName}/${defaultFolderName}`; await dialTest.step('Prepare a new conversation to publish', async () => { conversationToPublish = conversationData.prepareDefaultConversation(); @@ -279,11 +280,11 @@ dialAdminTest( await selectFolderModal.newFolderButton.click(); await selectFoldersAssertion.assertFolderEditInputState('visible'); await selectFoldersAssertion.assertFolderEditInputValue( - ExpectedConstants.newFolderWithIndexTitle(1), + defaultFolderName, ); await selectFolders.getEditFolderInputActions().clickTickButton(); await selectFoldersAssertion.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(1) }, + { name: defaultFolderName }, 'visible', ); }, @@ -292,9 +293,7 @@ dialAdminTest( await dialTest.step( 'Open folder dropdown menu and verify available options', async () => { - await selectFolders.openFolderDropdownMenu( - ExpectedConstants.newFolderWithIndexTitle(1), - ); + await selectFolders.openFolderDropdownMenu(defaultFolderName); await folderDropdownMenuAssertion.assertMenuOptions([ MenuOptions.rename, MenuOptions.delete, @@ -318,11 +317,11 @@ dialAdminTest( await selectFolders.openFolderDropdownMenu(cutNewFolderName); await folderDropdownMenu.selectMenuOption(MenuOptions.addNewFolder); await selectFoldersAssertion.assertFolderEditInputValue( - ExpectedConstants.newFolderWithIndexTitle(1), + defaultFolderName, ); await selectFolders.getEditFolderInputActions().clickTickButton(); await selectFoldersAssertion.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(1) }, + { name: defaultFolderName }, 'visible', ); }); @@ -331,9 +330,7 @@ dialAdminTest( 'Verify error message appears on adding more than 3 sub-folders', async () => { for (let i = 1; i <= maxNestedLevel - 1; i++) { - await selectFolders.openFolderDropdownMenu( - ExpectedConstants.newFolderWithIndexTitle(i), - ); + await selectFolders.openFolderDropdownMenu(defaultFolderName, i); await folderDropdownMenu.selectMenuOption(MenuOptions.addNewFolder); if (i === maxNestedLevel - 1) { await errorToastAssertion.assertToastMessage( @@ -343,7 +340,10 @@ dialAdminTest( } else { await selectFolders.getEditFolderInputActions().clickTickButton(); await selectFoldersAssertion.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(i + 1) }, + { + name: defaultFolderName, + index: i + 1, + }, 'visible', ); } @@ -371,16 +371,12 @@ dialAdminTest( if (i === maxNestedLevel - 1) { await selectFolders.getEditFolderInputActions().clickTickButton(); } - await selectFolderModal.selectFolder( - ExpectedConstants.newFolderWithIndexTitle(i), - ); + await selectFolderModal.selectFolder(defaultFolderName, i); await selectFoldersAssertion.assertFolderSelectedState( - { name: ExpectedConstants.newFolderWithIndexTitle(i) }, + { name: defaultFolderName, index: i }, true, ); - await selectFolders.expandFolder( - ExpectedConstants.newFolderWithIndexTitle(i), - ); + await selectFolders.expandFolder(defaultFolderName, undefined, i); } }, ); @@ -445,8 +441,6 @@ dialAdminTest( await adminPublicationReviewControl.backToPublicationRequest(); await adminPublishingApprovalModal.approveRequest(); - await dialHomePage.reloadPage(); - await dialHomePage.waitForPageLoaded(); await adminOrganizationFolderConversationAssertions.assertFolderState( { name: cutNewFolderName }, 'visible', @@ -457,17 +451,18 @@ dialAdminTest( ); for (let i = 1; i <= maxNestedLevel - 1; i++) { await adminOrganizationFolderConversationAssertions.assertFolderState( - { name: ExpectedConstants.newFolderWithIndexTitle(i) }, + { name: defaultFolderName, index: i }, 'visible', ); await adminOrganizationFolderConversations.expandFolder( - ExpectedConstants.newFolderWithIndexTitle(i), - { httpHost: ExpectedConstants.newFolderWithIndexTitle(i) }, + defaultFolderName, + { httpHost: defaultFolderName }, + i, ); } await adminOrganizationFolderConversationAssertions.assertFolderEntityState( - { name: ExpectedConstants.newFolderWithIndexTitle(3) }, + { name: defaultFolderName, index: 3 }, { name: conversationToPublish.name }, 'visible', ); diff --git a/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts b/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts index 90b0ac4baf..78f99222ff 100644 --- a/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts +++ b/apps/chat-e2e/src/tests/publishFolderWithConversation.test.ts @@ -491,14 +491,16 @@ dialAdminTest( for (let i = 1; i <= levelsCount - 2; i++) { await selectFolders.openFolderDropdownMenu( - ExpectedConstants.newFolderWithIndexTitle(i), + ExpectedConstants.newFolderWithIndexTitle(1), + i, ); await folderDropdownMenu.selectMenuOption(MenuOptions.addNewFolder); await selectFolders.getEditFolderInputActions().clickTickButton(); } await selectFolderModal.selectFolder( - ExpectedConstants.newFolderWithIndexTitle(levelsCount - 1), + ExpectedConstants.newFolderWithIndexTitle(1), + levelsCount - 1, ); await selectFolderModal.clickSelectFolderButton(); await errorToastAssertion.assertToastMessage( diff --git a/apps/chat-e2e/src/tests/selectUploadFolder.test.ts b/apps/chat-e2e/src/tests/selectUploadFolder.test.ts index 14ded2c9d4..62942374b6 100644 --- a/apps/chat-e2e/src/tests/selectUploadFolder.test.ts +++ b/apps/chat-e2e/src/tests/selectUploadFolder.test.ts @@ -91,7 +91,7 @@ dialTest( await dialTest.step( 'Select created folder and verify correct path is displayed in "Upload to" field, the field is highlighted and has text_overflow=ellipsis property', async () => { - await selectFolderModal.selectFolder(updatedFolderName, { + await selectFolderModal.selectFolder(updatedFolderName, 1, { triggeredApiHost: API.listingHost, }); await selectFolderModal.selectFolderButton.click(); diff --git a/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts b/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts index 129f3675e8..b2cb5cd54e 100644 --- a/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts +++ b/apps/chat-e2e/src/ui/webElements/selectFolderModal.ts @@ -54,9 +54,13 @@ export class SelectFolderModal extends BaseElement { public async selectFolder( folderName: string, + index?: number, { triggeredApiHost }: { triggeredApiHost?: string } = {}, ) { - const folderToSelect = this.getSelectFolders().getFolderName(folderName); + const folderToSelect = this.getSelectFolders().getFolderName( + folderName, + index, + ); let respPremise: Promise; if (triggeredApiHost) { respPremise = this.page.waitForResponse((r) => diff --git a/apps/chat/src/components/Chat/ChangePathDialog.tsx b/apps/chat/src/components/Chat/ChangePathDialog.tsx index ae49d43211..5497466bb8 100644 --- a/apps/chat/src/components/Chat/ChangePathDialog.tsx +++ b/apps/chat/src/components/Chat/ChangePathDialog.tsx @@ -2,6 +2,7 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; +import { updateEntitiesFoldersAndIds } from '@/src/utils/app/common'; import { constructPath } from '@/src/utils/app/file'; import { getChildAndCurrentFoldersIdsById, @@ -9,6 +10,7 @@ import { getNextDefaultName, getPathToFolderById, sortByName, + updateMovedFolderId, validateFolderRenaming, } from '@/src/utils/app/folders'; @@ -160,10 +162,17 @@ export const ChangePathDialog = ({ setErrorMessage(t(error) as string); return; } + const { updatedOpenedFoldersIds } = updateEntitiesFoldersAndIds( + [], + [], + (id) => updateMovedFolderId(folderId, newFolderId, id), + openedFoldersIds, + ); dispatch(actions.renameTemporaryFolder({ folderId, name: newName })); + setOpenedFoldersIds(updatedOpenedFoldersIds); }, - [actions, dispatch, folders, t], + [actions, dispatch, folders, t, openedFoldersIds, setOpenedFoldersIds], ); const handleAddFolder = useCallback( @@ -175,12 +184,15 @@ export const ChangePathDialog = ({ false, true, ); + const id = constructPath(parentFolderId, folderName); - setSelectedFolderId(constructPath(parentFolderId, folderName)); + setSelectedFolderId(id); dispatch( actions.createTemporaryFolder({ - relativePath: parentFolderId, + folderId: parentFolderId, + name: folderName, + id, }), ); diff --git a/apps/chat/src/store/conversations/conversations.reducers.ts b/apps/chat/src/store/conversations/conversations.reducers.ts index a6b7c1d850..a46851ad6e 100644 --- a/apps/chat/src/store/conversations/conversations.reducers.ts +++ b/apps/chat/src/store/conversations/conversations.reducers.ts @@ -3,11 +3,10 @@ import { PlotParams } from 'react-plotly.js'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { combineEntities } from '@/src/utils/app/common'; -import { constructPath } from '@/src/utils/app/file'; import { addGeneratedFolderId, - getNextDefaultName, isFolderEmpty, + renameFolderWithChildren, } from '@/src/utils/app/folders'; import { getConversationRootId, @@ -15,15 +14,12 @@ import { isEntityIdLocal, } from '@/src/utils/app/id'; import { doesEntityContainSearchTerm } from '@/src/utils/app/search'; -import { translate } from '@/src/utils/app/translation'; import { Conversation } from '@/src/types/chat'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { SearchFilters } from '@/src/types/search'; import { LastConversationSettings } from '@/src/types/settings'; -import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; - import * as ConversationsSelectors from './conversations.selectors'; import { ConversationsState } from './conversations.types'; @@ -394,32 +390,19 @@ export const conversationsSlice = createSlice({ { payload, }: PayloadAction<{ - relativePath?: string; + name: string; + id: string; + folderId?: string; }>, ) => { - const folderName = getNextDefaultName( - translate(DEFAULT_FOLDER_NAME), - [ - ...state.temporaryFolders, - ...state.folders.filter((folder) => folder.publishedWithMe), - ], - 0, - false, - true, - ); - const id = constructPath( - payload.relativePath || getConversationRootId(), - folderName, - ); - state.temporaryFolders.push({ - id, - name: folderName, + id: payload.id, + name: payload.name, type: FolderType.Chat, - folderId: payload.relativePath || getConversationRootId(), + folderId: payload.folderId || getConversationRootId(), temporary: true, }); - state.newAddedFolderId = id; + state.newAddedFolderId = payload.id; }, deleteFolder: (state, _action: PayloadAction<{ folderId?: string }>) => state, @@ -439,13 +422,11 @@ export const conversationsSlice = createSlice({ { payload }: PayloadAction<{ folderId: string; name: string }>, ) => { state.newAddedFolderId = undefined; - const name = payload.name.trim(); - - state.temporaryFolders = state.temporaryFolders.map((folder) => - folder.id !== payload.folderId - ? folder - : { ...folder, name, id: constructPath(folder.folderId, name) }, - ); + state.temporaryFolders = renameFolderWithChildren({ + folderId: payload.folderId, + newName: payload.name, + folders: state.temporaryFolders, + }); }, resetNewFolderId: (state) => { state.newAddedFolderId = undefined; diff --git a/apps/chat/src/store/prompts/prompts.reducers.ts b/apps/chat/src/store/prompts/prompts.reducers.ts index 08711f16cc..9b466fd516 100644 --- a/apps/chat/src/store/prompts/prompts.reducers.ts +++ b/apps/chat/src/store/prompts/prompts.reducers.ts @@ -1,23 +1,19 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { combineEntities } from '@/src/utils/app/common'; -import { constructPath } from '@/src/utils/app/file'; import { addGeneratedFolderId, - getNextDefaultName, isFolderEmpty, + renameFolderWithChildren, } from '@/src/utils/app/folders'; import { getPromptRootId, isEntityIdExternal } from '@/src/utils/app/id'; import { doesEntityContainSearchTerm } from '@/src/utils/app/search'; -import { translate } from '@/src/utils/app/translation'; import { FolderInterface, FolderType } from '@/src/types/folder'; import { Prompt, PromptInfo } from '@/src/types/prompt'; import { SearchFilters } from '@/src/types/search'; import '@/src/types/share'; -import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; - import * as PromptsSelectors from './prompts.selectors'; import { PromptsState } from './prompts.types'; @@ -205,32 +201,19 @@ export const promptsSlice = createSlice({ { payload, }: PayloadAction<{ - relativePath: string; + name: string; + id: string; + folderId?: string; }>, ) => { - const folderName = getNextDefaultName( - translate(DEFAULT_FOLDER_NAME), - [ - ...state.temporaryFolders, - ...state.folders.filter((folder) => folder.publishedWithMe), - ], - 0, - false, - true, - ); - const id = constructPath( - payload.relativePath || getPromptRootId(), - folderName, - ); - state.temporaryFolders.push({ - id, - name: folderName, + id: payload.id, + name: payload.name, type: FolderType.Prompt, - folderId: payload.relativePath || getPromptRootId(), + folderId: payload.folderId || getPromptRootId(), temporary: true, }); - state.newAddedFolderId = id; + state.newAddedFolderId = payload.id; }, deleteFolder: ( state, @@ -254,13 +237,11 @@ export const promptsSlice = createSlice({ { payload }: PayloadAction<{ folderId: string; name: string }>, ) => { state.newAddedFolderId = undefined; - const name = payload.name.trim(); - - state.temporaryFolders = state.temporaryFolders.map((folder) => - folder.id !== payload.folderId - ? folder - : { ...folder, name, id: constructPath(folder.folderId, name) }, - ); + state.temporaryFolders = renameFolderWithChildren({ + folderId: payload.folderId, + newName: payload.name, + folders: state.temporaryFolders, + }); }, resetNewFolderId: (state) => { state.newAddedFolderId = undefined; diff --git a/apps/chat/src/utils/app/folders.ts b/apps/chat/src/utils/app/folders.ts index 19b73bbd0c..188dc94f9c 100644 --- a/apps/chat/src/utils/app/folders.ts +++ b/apps/chat/src/utils/app/folders.ts @@ -14,7 +14,11 @@ import { EntityFilters } from '@/src/types/search'; import { DEFAULT_FOLDER_NAME } from '@/src/constants/default-ui-settings'; -import { doesHaveDotsInTheEnd, prepareEntityName } from './common'; +import { + doesHaveDotsInTheEnd, + prepareEntityName, + updateEntitiesFoldersAndIds, +} from './common'; import { isRootId } from './id'; import { @@ -25,6 +29,7 @@ import { UploadStatus, } from '@epam/ai-dial-shared'; import escapeRegExp from 'lodash-es/escapeRegExp'; +import groupBy from 'lodash-es/groupBy'; import sortBy from 'lodash-es/sortBy'; import uniq from 'lodash-es/uniq'; @@ -564,3 +569,33 @@ export const isFolderEmpty = ({ !entities.some((entity) => entity.folderId === id) ); }; + +export const renameFolderWithChildren = ({ + folderId, + newName, + folders, +}: { + folderId: string; + newName: string; + folders: FolderInterface[]; +}) => { + const { + target: [targetFolder], + otherFolders = [], + } = groupBy(folders, (f) => (f.id === folderId ? 'target' : 'otherFolders')); + + if (!targetFolder) return folders; + + const newFolder = addGeneratedFolderId({ + ...targetFolder, + name: newName.trim(), + }); + const { updatedFolders } = updateEntitiesFoldersAndIds( + [], + otherFolders, + (id) => updateMovedFolderId(folderId, newFolder.id, id), + [], + ); + + return updatedFolders.concat(newFolder); +};