diff --git a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx index 6795496c1d..d9706876f6 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/Name.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/Name.tsx @@ -14,6 +14,7 @@ import { removeSpecialChars } from "utils/helpers"; import { AppState } from "reducers"; import { Page, ReduxActionTypes } from "constants/ReduxActionConstants"; import { Colors } from "constants/Colors"; +import { WidgetTypes } from "constants/WidgetConstants"; const searchHighlightSpanClassName = "token"; const searchTokenizationDelimiter = "!!"; @@ -71,6 +72,24 @@ export interface EntityNameProps { export const EntityName = forwardRef( (props: EntityNameProps, ref: React.Ref) => { const { name, updateEntityName, searchKeyword } = props; + const tabs: + | Array<{ id: string; widgetId: string; label: string }> + | undefined = useSelector((state: AppState) => { + if (state.entities.canvasWidgets.hasOwnProperty(props.entityId)) { + const widget = state.entities.canvasWidgets[props.entityId]; + if ( + widget.parentId && + state.entities.canvasWidgets.hasOwnProperty(widget.parentId) + ) { + const parent = state.entities.canvasWidgets[widget.parentId]; + if (parent.type === WidgetTypes.TABS_WIDGET) { + return parent.tabs; + } + } + } + return; + }); + const nameUpdateError = useSelector((state: AppState) => { return state.ui.explorer.updateEntityError === props.entityId; }); @@ -99,12 +118,20 @@ export const EntityName = forwardRef( ); const hasNameConflict = useCallback( - (newName: string) => - !( - existingPageNames.indexOf(newName) === -1 && - existingActionNames.indexOf(newName) === -1 && - existingWidgetNames.indexOf(newName) === -1 - ), + ( + newName: string, + tabs?: Array<{ id: string; widgetId: string; label: string }>, + ) => { + if (tabs === undefined) { + return !( + existingPageNames.indexOf(newName) === -1 && + existingActionNames.indexOf(newName) === -1 && + existingWidgetNames.indexOf(newName) === -1 + ); + } else { + return tabs.findIndex((tab) => tab.label === newName) > -1; + } + }, [existingPageNames, existingActionNames, existingWidgetNames], ); @@ -112,7 +139,7 @@ export const EntityName = forwardRef( (newName: string): string | boolean => { if (!newName || newName.trim().length === 0) { return "Please enter a name"; - } else if (newName !== name && hasNameConflict(newName)) { + } else if (newName !== name && hasNameConflict(newName, tabs)) { return `${newName} is already being used.`; } return false; diff --git a/app/client/src/sagas/PageSagas.tsx b/app/client/src/sagas/PageSagas.tsx index 36a2c4a686..93f8932804 100644 --- a/app/client/src/sagas/PageSagas.tsx +++ b/app/client/src/sagas/PageSagas.tsx @@ -19,6 +19,7 @@ import { initCanvasLayout, updateCurrentPage, updateWidgetNameSuccess, + updateAndSaveLayout, } from "actions/pageActions"; import PageApi, { ClonePageRequest, @@ -75,6 +76,7 @@ import { getQueryParams } from "utils/AppsmithUtils"; import PerformanceTracker, { PerformanceTransactionName, } from "utils/PerformanceTracker"; +import { WidgetTypes } from "constants/WidgetConstants"; const getWidgetName = (state: AppState, widgetId: string) => state.entities.canvasWidgets[widgetId]; @@ -544,47 +546,110 @@ export function* updateWidgetNameSaga( const pageId = yield select(getCurrentPageId); const existingPageNames = yield select(getExistingPageNames); - // check if name is not conflicting with any - // existing entity/api/queries/reserved words - if ( - isNameValid(action.payload.newName, { - ...evalTree, - ...existingPageNames, - }) - ) { - const request: UpdateWidgetNameRequest = { - newName: action.payload.newName, - oldName: widgetName, - pageId, - layoutId, - }; - const response: UpdateWidgetNameResponse = yield call( - PageApi.updateWidgetName, - request, - ); - const isValidResponse = yield validateResponse(response); - if (isValidResponse) { - yield updateCanvasWithDSL(response.data, pageId, layoutId); + // TODO(abhinav): Why do we need to jump through these hoops just to + // change the tab name? Figure out a better design to make this moot. + const tabs: + | Array<{ + id: string; + widgetId: string; + label: string; + }> + | undefined = yield select((state: AppState) => { + // Check if this widget exists in the canvas widgets + if (state.entities.canvasWidgets.hasOwnProperty(action.payload.id)) { + // If it does assign it to a variable + const widget = state.entities.canvasWidgets[action.payload.id]; + // Check if this widget has a parent in the canvas widgets + if ( + widget.parentId && + state.entities.canvasWidgets.hasOwnProperty(widget.parentId) + ) { + // If the parent exists assign it to a variable + const parent = state.entities.canvasWidgets[widget.parentId]; + // Check if this parent is a TABS_WIDGET + if (parent.type === WidgetTypes.TABS_WIDGET) { + // If it is return the tabs property + return parent.tabs; + } + } + } + // This isn't a tab in a tabs widget so return undefined + return; + }); - yield put(updateWidgetNameSuccess()); - // Add this to the page DSLs for entity explorer + // If we're trying to update the name of a tab in the TABS_WIDGET + if (tabs !== undefined) { + // Get all canvas widgets + const stateWidgets = yield select(getWidgets); + // Shallow copy canvas widgets as they're immutable + const widgets = { ...stateWidgets }; + // Get the parent Id of the tab (canvas widget) whose name we're updating + const parentId = widgets[action.payload.id].parentId; + // Update the tabName property of the tab (canvas widget) + widgets[action.payload.id] = { + ...widgets[action.payload.id], + tabName: action.payload.newName, + }; + // Shallow copy the parent widget so that we can update the properties + const parent = { ...widgets[parentId] }; + // Update the tabs property of the parent tabs widget + parent.tabs = tabs.map( + (tab: { widgetId: string; label: string; id: string }) => { + if (tab.widgetId === action.payload.id) { + return { ...tab, label: action.payload.newName }; + } + return tab; + }, + ); + // replace the parent widget in the canvas widgets + widgets[parentId] = parent; + // Update and save the new widgets + yield put(updateAndSaveLayout(widgets)); + // Send a update saying that we've successfully updated the name + yield put(updateWidgetNameSuccess()); + } else { + // check if name is not conflicting with any + // existing entity/api/queries/reserved words + if ( + isNameValid(action.payload.newName, { + ...evalTree, + ...existingPageNames, + }) + ) { + const request: UpdateWidgetNameRequest = { + newName: action.payload.newName, + oldName: widgetName, + pageId, + layoutId, + }; + const response: UpdateWidgetNameResponse = yield call( + PageApi.updateWidgetName, + request, + ); + const isValidResponse = yield validateResponse(response); + if (isValidResponse) { + yield updateCanvasWithDSL(response.data, pageId, layoutId); + + yield put(updateWidgetNameSuccess()); + // Add this to the page DSLs for entity explorer + yield put({ + type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, + payload: { + pageId: pageId, + dsl: response.data.dsl, + }, + }); + } + } else { yield put({ - type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, + type: ReduxActionErrorTypes.UPDATE_WIDGET_NAME_ERROR, payload: { - pageId: pageId, - dsl: response.data.dsl, + error: { + message: `Entity name: ${action.payload.newName} is already being used.`, + }, }, }); } - } else { - yield put({ - type: ReduxActionErrorTypes.UPDATE_WIDGET_NAME_ERROR, - payload: { - error: { - message: `Entity name: ${action.payload.newName} is already being used.`, - }, - }, - }); } } catch (error) { yield put({