import type { DefaultRootState } from "react-redux"; import type { ReduxAction } from "actions/ReduxActionTypes"; import { ReduxActionErrorTypes, ReduxActionTypes, } from "ee/constants/ReduxActionConstants"; import type { ClonePageActionPayload, CreatePageActionPayload, DeletePageActionPayload, FetchPageActionPayload, FetchPublishedPageActionPayload, FetchPublishedPageResourcesPayload, GenerateTemplatePageActionPayload, SetPageOrderActionPayload, SetupPageActionPayload, SetupPublishedPageActionPayload, UpdateCanvasPayload, UpdatePageActionPayload, } from "actions/pageActions"; import { createPageAction, fetchPageAction, fetchPublishedPageAction, } from "actions/pageActions"; import { clonePageSuccess, deletePageSuccess, fetchAllPageEntityCompletion, fetchPageSuccess, fetchPublishedPageSuccess, generateTemplateError, generateTemplateSuccess, initCanvasLayout, saveLayout, savePageSuccess, setLastUpdatedTime, setUrlData, updateAndSaveLayout, updateCurrentPage, updatePageError, updatePageSuccess, updateWidgetNameSuccess, } from "actions/pageActions"; import type { FetchPageRequest, FetchPageResponse, FetchPageResponseData, GenerateTemplatePageRequest, PageLayout, PageLayoutsRequest, SavePageRequest, SavePageResponse, SavePageResponseData, UpdatePageResponse, UpdateWidgetNameRequest, UpdateWidgetNameResponse, } from "api/PageApi"; import PageApi from "api/PageApi"; import type { CanvasWidgetsReduxState } from "ee/reducers/entityReducers/canvasWidgetsReducer"; import { all, call, put, select, take } from "redux-saga/effects"; import history from "utils/history"; import { isNameValid } from "utils/helpers"; import { extractCurrentDSL } from "utils/WidgetPropsUtils"; import { getAllPageIdentities, getDefaultBasePageId, getDefaultPageId, getEditorConfigs, getWidgets, } from "sagas/selectors"; import { IncorrectBindingError, validateResponse } from "sagas/ErrorSagas"; import type { ApiResponse } from "api/ApiResponses"; import { getCurrentApplicationId, getCurrentLayoutId, getCurrentPageId, getCurrentPageName, getMainCanvasProps, getPageById, } from "selectors/editorSelectors"; import { executePageLoadActions, fetchActionsForPage, fetchActionsForPageError, fetchActionsForPageSuccess, fetchActionsForView, setActionsRunBehaviour, setJSActionsRunBehaviour, } from "actions/pluginActionActions"; import type { UrlDataState } from "reducers/entityReducers/appReducer"; import { APP_MODE } from "entities/App"; import { clearEvalCache } from "../../sagas/EvaluationsSaga"; import { getQueryParams } from "utils/URLUtils"; import log from "loglevel"; import { migrateIncorrectDynamicBindingPathLists } from "utils/migrations/IncorrectDynamicBindingPathLists"; import { ERROR_CODES } from "ee/constants/ApiConstants"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import DEFAULT_TEMPLATE from "templates/default"; import { getAppMode } from "ee/selectors/applicationSelectors"; import { setCrudInfoModalData } from "actions/crudInfoModalActions"; import { selectWidgetInitAction } from "actions/widgetSelectionActions"; import { fetchJSCollectionsForPage, fetchJSCollectionsForPageError, fetchJSCollectionsForPageSuccess, } from "actions/jsActionActions"; import WidgetFactory from "WidgetProvider/factory"; import { builderURL } from "ee/RouteBuilder"; import { failFastApiCalls, waitForWidgetConfigBuild } from "sagas/InitSagas"; import { type InitConsolidatedApi } from "sagas/InitSagas"; import { resizePublishedMainCanvasToLowestWidget } from "sagas/WidgetOperationUtils"; import { checkAndLogErrorsIfCyclicDependency, getFromServerWhenNoPrefetchedResult, } from "sagas/helper"; import { LOCAL_STORAGE_KEYS } from "utils/localStorage"; import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions"; import { getUsedActionNames } from "selectors/actionSelectors"; import { getPageList } from "ee/selectors/entitiesSelector"; import { setPreviewModeAction } from "actions/editorActions"; import { SelectionRequestType } from "sagas/WidgetSelectUtils"; import { toast } from "@appsmith/ads"; import type { MainCanvasReduxState } from "ee/reducers/uiReducers/mainCanvasReducer"; import { UserCancelledActionExecutionError } from "sagas/ActionExecution/errorUtils"; import { getInstanceId } from "ee/selectors/organizationSelectors"; import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; import type { WidgetProps } from "widgets/BaseWidget"; import { nestDSL, flattenDSL, LATEST_DSL_VERSION } from "@shared/dsl"; import { fetchSnapshotDetailsAction } from "actions/autoLayoutActions"; import { selectFeatureFlags } from "ee/selectors/featureFlagsSelectors"; import { isGACEnabled } from "ee/utils/planHelpers"; import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; import { getLayoutSystemType } from "selectors/layoutSystemSelectors"; import { getLayoutSystemDSLTransformer } from "layoutSystems/common/utils/LayoutSystemDSLTransformer"; import type { DSLWidget } from "WidgetProvider/constants"; import type { FeatureFlags } from "ee/entities/FeatureFlag"; import { getCurrentWorkspaceId } from "ee/selectors/selectedWorkspaceSelectors"; import { ActionExecutionContext } from "entities/Action"; import type { LayoutSystemTypes } from "layoutSystems/types"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; import { convertToBasePageIdSelector } from "selectors/pageListSelectors"; import type { Page } from "entities/Page"; import { ConsolidatedPageLoadApi } from "api"; import { selectCombinedPreviewMode, selectGitApplicationCurrentBranch, } from "selectors/gitModSelectors"; import { appsmithTelemetry } from "instrumentation"; import { getLayoutSavePayload } from "ee/sagas/helpers"; import { apiFailureResponseInterceptor } from "api/interceptors/response"; import type { AxiosError } from "axios"; import { handleFetchApplicationError } from "./ApplicationSagas"; import { getCurrentUser } from "actions/authActions"; export interface HandleWidgetNameUpdatePayload { newName: string; widgetName: string; } export const checkIfMigrationIsNeeded = ( fetchPageResponse?: FetchPageResponse, ) => { const currentDSL = fetchPageResponse?.data.layouts[0].dsl; if (!currentDSL) return false; return currentDSL.version !== LATEST_DSL_VERSION; }; export const WidgetTypes = WidgetFactory.widgetTypes; export const getWidgetName = (state: DefaultRootState, widgetId: string) => state.entities.canvasWidgets[widgetId]; //Method to load the default page if current page is not found export function* refreshTheApp() { try { const currentPageId: string = yield select(getCurrentPageId); const defaultBasePageId: string = yield select(getDefaultBasePageId); const pagesList: Page[] = yield select(getPageList); const gitBranch: string | undefined = yield select( selectGitApplicationCurrentBranch, ); const isCurrentPageIdInList = pagesList.filter((page) => page.pageId === currentPageId).length > 0; if (isCurrentPageIdInList) { location.reload(); } else { location.assign( builderURL({ basePageId: defaultBasePageId, branch: gitBranch, }), ); } } catch (error) { log.error(error); location.reload(); } } export const getCanvasWidgetsPayload = async ( pageResponse: FetchPageResponse, dslTransformer?: (dsl: DSLWidget) => DSLWidget, ): Promise => { const currentDSL = await extractCurrentDSL({ dslTransformer, response: pageResponse, }); const extractedDSL = currentDSL.dsl; const flattenedDSL = flattenDSL(extractedDSL); const pageWidgetId = MAIN_CONTAINER_WIDGET_ID; return { pageWidgetId, currentPageName: pageResponse.data.name, currentPageId: pageResponse.data.id, dsl: extractedDSL, widgets: flattenedDSL, currentLayoutId: pageResponse.data.layouts[0].id, // TODO(abhinav): Handle for multiple layouts currentApplicationId: pageResponse.data.applicationId, pageActions: pageResponse.data.layouts[0].layoutOnLoadActions || [], layoutOnLoadActionErrors: pageResponse.data.layouts[0].layoutOnLoadActionErrors || [], }; }; export function* handleFetchedPage({ fetchPageResponse, isFirstLoad = false, pageId, }: { fetchPageResponse: FetchPageResponse; pageId: string; isFirstLoad?: boolean; }) { const layoutSystemType: LayoutSystemTypes = yield select(getLayoutSystemType); const isAnvilLayout: boolean = yield select(getIsAnvilLayout); const mainCanvasProps: MainCanvasReduxState = yield select(getMainCanvasProps); const dslTransformer = getLayoutSystemDSLTransformer( layoutSystemType, mainCanvasProps.width, ); const isValidResponse: boolean = yield validateResponse(fetchPageResponse); const willPageBeMigrated = checkIfMigrationIsNeeded(fetchPageResponse); const lastUpdatedTime = getLastUpdateTime(fetchPageResponse); const pageSlug = fetchPageResponse.data.slug; const pagePermissions = fetchPageResponse.data.userPermissions; if (isValidResponse) { // Clear any existing caches yield call(clearEvalCache); // Set url params yield call(setDataUrl); // Wait for widget config to be loaded before we can generate the canvas payload yield call(waitForWidgetConfigBuild); // Get Canvas payload const canvasWidgetsPayload: UpdateCanvasPayload = yield getCanvasWidgetsPayload(fetchPageResponse, dslTransformer); // Update the canvas yield put(initCanvasLayout(canvasWidgetsPayload)); // fetch snapshot API yield put(fetchSnapshotDetailsAction()); // set current page yield put(updateCurrentPage(pageId, pageSlug, pagePermissions)); // dispatch fetch page success yield put(fetchPageSuccess()); /* Currently, All Actions are fetched in initSagas and on pageSwitch we only fetch page */ // Hence, if is not isFirstLoad then trigger evaluation with execute pageLoad action if (!isFirstLoad) { yield put(fetchAllPageEntityCompletion([executePageLoadActions()])); } // Sets last updated time yield put(setLastUpdatedTime(lastUpdatedTime)); yield put({ type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE, payload: canvasWidgetsPayload.dsl, }); // Since new page has new layout, we need to generate a data structure // to compute dynamic height based on the new layout. yield put(generateAutoHeightLayoutTreeAction(true, true)); // If the type of the layoutSystem is ANVIL, then we need to save the layout // This is because we have updated the DSL // using the AnvilDSLTransformer when we called the getCanvasWidgetsPayload function if (willPageBeMigrated || isAnvilLayout) { yield put(saveLayout()); } } } export const getLastUpdateTime = (pageResponse: FetchPageResponse): number => pageResponse.data.lastUpdatedTime; export function* fetchPageSaga(action: ReduxAction) { try { const { id: pageId, isFirstLoad = false, pageWithMigratedDsl, } = action.payload; const params: FetchPageRequest = { pageId, migrateDSL: true }; const fetchPageResponse: FetchPageResponse = yield call( getFromServerWhenNoPrefetchedResult, pageWithMigratedDsl, () => call(PageApi.fetchPage, params), ); yield handleFetchedPage({ fetchPageResponse, pageId, isFirstLoad, }); } catch (error) { log.error(error); yield put({ type: ReduxActionErrorTypes.FETCH_PAGE_ERROR, payload: { error, }, }); } } export function* updateCanvasLayout(response: FetchPageResponse) { // Wait for widget config to load before we can get the canvas payload yield call(waitForWidgetConfigBuild); // Get Canvas payload const canvasWidgetsPayload: UpdateCanvasPayload = yield getCanvasWidgetsPayload(response); // resize main canvas resizePublishedMainCanvasToLowestWidget(canvasWidgetsPayload.widgets); // Update the canvas yield put(initCanvasLayout(canvasWidgetsPayload)); // Since new page has new layout, we need to generate a data structure // to compute dynamic height based on the new layout. yield put(generateAutoHeightLayoutTreeAction(true, true)); } export function* postFetchedPublishedPage( response: FetchPageResponse, pageId: string, ) { // set current page yield put( updateCurrentPage( pageId, response.data.slug, response.data.userPermissions, ), ); // Clear any existing caches yield call(clearEvalCache); // Set url params yield call(setDataUrl); yield call(updateCanvasLayout, response); } export function* fetchPublishedPageSaga( action: ReduxAction, ) { try { const { bustCache, pageId, pageWithMigratedDsl } = action.payload; const params = { pageId, bustCache }; const response: FetchPageResponse = yield call( getFromServerWhenNoPrefetchedResult, pageWithMigratedDsl, () => call(PageApi.fetchPublishedPage, params), ); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { yield call(postFetchedPublishedPage, response, pageId); yield put(fetchPublishedPageSuccess()); } } catch (error) { yield put({ type: ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_ERROR, payload: { error, }, }); } } export function* fetchPublishedPageResourcesSaga( action: ReduxAction, ) { try { const { basePageId, branch: branchName, pageId } = action.payload; const initConsolidatedApiResponse: ApiResponse = yield ConsolidatedPageLoadApi.getConsolidatedPageLoadDataView({ defaultPageId: basePageId, branchName, }); const isValidResponse: boolean = yield validateResponse( initConsolidatedApiResponse, ); const response: InitConsolidatedApi | undefined = initConsolidatedApiResponse.data; if (isValidResponse) { // We need to recall consolidated view API in order to fetch actions when page is switched // As in the first call only actions of the current page are fetched // In future, we can reuse this saga to fetch other resources of the page like actionCollections etc const { pageWithMigratedDsl, publishedActions, userProfile } = response; // Update the current user in the redux store. If the session has expired between page switch, the user profile will be updated yield put(getCurrentUser(userProfile)); // If the pageWithMigratedDsl has an error, we need to intercept the error and return // This makes sure that the user is navigated to the login page if page is not found if (pageWithMigratedDsl?.responseMeta?.error) { const { responseMeta } = pageWithMigratedDsl; const { status } = responseMeta; // apiFailureResponseInterceptor throws an error if the response is not valid // this error needs to be caught and handled by handleFetchApplicationError yield apiFailureResponseInterceptor({ response: { data: { responseMeta, }, status, }, } as AxiosError); } yield call(postFetchedPublishedPage, pageWithMigratedDsl, pageId); // NOTE: fetchActionsForView is used here to update publishedActions in redux store and not to fetch actions again yield put(fetchActionsForView({ applicationId: "", publishedActions })); yield put(fetchAllPageEntityCompletion([executePageLoadActions()])); yield put({ type: ReduxActionTypes.FETCH_PUBLISHED_PAGE_RESOURCES_SUCCESS, }); } } catch (error) { // Handle the error thrown by apiFailureResponseInterceptor yield call(handleFetchApplicationError, error); yield put({ type: ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_RESOURCES_ERROR, payload: { error, }, }); } } export function* fetchAllPublishedPagesSaga() { try { const pageIdentities: { pageId: string; basePageId: string }[] = yield select(getAllPageIdentities); yield all( pageIdentities.map((pageIdentity) => { return call(PageApi.fetchPublishedPage, { pageId: pageIdentity.pageId, bustCache: true, }); }), ); } catch (error) { log.error({ error }); } } export function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) { const widgets: CanvasWidgetsReduxState = yield select(getWidgets); const editorConfigs: | { applicationId: string; pageId: string; layoutId: string; } // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any | undefined = yield select(getEditorConfigs) as any; if (!editorConfigs) return; const savePageRequest: SavePageRequest = yield call( getLayoutSavePayload, widgets, editorConfigs, ); try { // Store the updated DSL in the pageDSLs reducer yield put({ type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, payload: { pageId: savePageRequest.pageId, dsl: savePageRequest.dsl, layoutId: savePageRequest.layoutId, }, }); yield put({ type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE, payload: savePageRequest.dsl, }); /** * TODO: Reactivate the capturing or remove this block * once the below issue has been fixed. Commenting to avoid * Sentry quota to fill up * https://github.com/appsmithorg/appsmith/issues/20744 */ // captureInvalidDynamicBindingPath( // nestDSL(widgets), // ); const savePageResponse: SavePageResponse = yield call( PageApi.savePage, savePageRequest, ); const isValidResponse: boolean = yield validateResponse(savePageResponse); if (isValidResponse) { const { actionUpdates, messages } = savePageResponse.data; // We do not want to show these toasts in guided tour // Show toast messages from the server if (messages && messages.length) { savePageResponse.data.messages.forEach((message) => { toast.show(message, { kind: "info", }); }); } // Update actions if (actionUpdates && actionUpdates.length > 0) { const actions = actionUpdates.filter( (d) => !d.hasOwnProperty("collectionId"), ); if (actions && actions.length) { yield put(setActionsRunBehaviour(actions)); } const jsActions = actionUpdates.filter((d) => d.hasOwnProperty("collectionId"), ); if (jsActions && jsActions.length) { yield put(setJSActionsRunBehaviour(jsActions)); } } yield put(setLastUpdatedTime(Date.now() / 1000)); yield put(savePageSuccess(savePageResponse)); checkAndLogErrorsIfCyclicDependency( (savePageResponse.data as SavePageResponseData) .layoutOnLoadActionErrors, ); } } catch (error) { if (error instanceof UserCancelledActionExecutionError) { return; } yield put({ type: ReduxActionErrorTypes.SAVE_PAGE_ERROR, payload: { error, show: false, }, }); if (error instanceof IncorrectBindingError) { const { isRetry } = action?.payload; const incorrectBindingError = JSON.parse(error.message); const { message } = incorrectBindingError; if (isRetry) { appsmithTelemetry.captureException( new Error("Failed to correct binding paths"), { errorName: "PageSagas_BindingPathCorrection", }, ); yield put({ type: ReduxActionErrorTypes.FAILED_CORRECTING_BINDING_PATHS, payload: { error: { message, code: ERROR_CODES.FAILED_TO_CORRECT_BINDING, crash: true, }, }, }); } else { // Create a nested structure because the migration needs the children in the dsl form const nestedDSL = nestDSL(widgets); const correctedWidgets = migrateIncorrectDynamicBindingPathLists(nestedDSL); // Flatten the widgets because the save page needs it in the flat structure const normalizedWidgets = flattenDSL(correctedWidgets); AnalyticsUtil.logEvent("CORRECT_BAD_BINDING", { error: error.message, correctWidget: JSON.stringify(normalizedWidgets), }); yield put( updateAndSaveLayout(normalizedWidgets, { isRetry: true, }), ); } } } } export function* saveAllPagesSaga(pageLayouts: PageLayoutsRequest[]) { let response: ApiResponse | undefined; try { const applicationId: string = yield select(getCurrentApplicationId); response = yield PageApi.saveAllPages(applicationId, pageLayouts); const isValidResponse: boolean = yield validateResponse(response, false); if (isValidResponse) { return true; } else { throw new Error(`Error while saving all pages, ${response?.data}`); } } catch (error) { throw error; } } export function* saveLayoutSaga(action: ReduxAction<{ isRetry?: boolean }>) { try { const currentPageId: string = yield select(getCurrentPageId); const currentPage: Page = yield select(getPageById(currentPageId)); const isPreviewMode: boolean = yield select(selectCombinedPreviewMode); const appMode: APP_MODE | undefined = yield select(getAppMode); const featureFlags: FeatureFlags = yield select(selectFeatureFlags); const isFeatureEnabled = isGACEnabled(featureFlags); if ( !getHasManagePagePermission( isFeatureEnabled, currentPage?.userPermissions || [], ) && appMode === APP_MODE.EDIT ) { yield validateResponse({ status: 403, resourceType: "Page", resourceId: currentPage.pageId, }); } if (appMode === APP_MODE.EDIT && !isPreviewMode) { yield put(saveLayout(action.payload.isRetry)); } } catch (error) { yield put({ type: ReduxActionErrorTypes.SAVE_PAGE_ERROR, payload: { error, }, }); } } export function* createNewPageFromEntity( action: ReduxAction, ) { try { const layoutSystemType: LayoutSystemTypes = yield select(getLayoutSystemType); const mainCanvasProps: MainCanvasReduxState = yield select(getMainCanvasProps); const dslTransformer = getLayoutSystemDSLTransformer( layoutSystemType, mainCanvasProps.width, ); // This saga is called when creating a new page from the entity explorer // In this flow, the server doesn't have a page DSL to return // So, the client premptively uses the default page DSL // The default page DSL is used and modified using the layout system // specific dslTransformer const currentDSL: { dsl: DSLWidget; layoutId: string | undefined } = yield extractCurrentDSL({ dslTransformer }); const defaultPageLayouts = [ { dsl: currentDSL.dsl, layoutOnLoadActions: [], }, ]; const { applicationId, name } = action?.payload || {}; const workspaceId: string = yield select(getCurrentWorkspaceId); const instanceId: string | undefined = yield select(getInstanceId); // So far this saga has only done the prep work to create a page // It generates and structures the parameters needed for creating a page // At the end we call the `createPage` saga that actually calls the API to // create a page yield put( createPageAction( applicationId, name, defaultPageLayouts, workspaceId, instanceId, ), ); } catch (error) { yield put({ type: ReduxActionErrorTypes.CREATE_PAGE_ERROR, payload: { error, }, }); } } export function* createPageSaga(action: ReduxAction) { try { const layoutSystemType: LayoutSystemTypes = yield select(getLayoutSystemType); const mainCanvasProps: MainCanvasReduxState = yield select(getMainCanvasProps); const dslTransformer = getLayoutSystemDSLTransformer( layoutSystemType, mainCanvasProps.width, ); const params = { ...action.payload }; const response: FetchPageResponse = yield call(PageApi.createPage, params); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { yield put({ type: ReduxActionTypes.CREATE_PAGE_SUCCESS, payload: { pageId: response.data.id, basePageId: response.data.baseId, pageName: response.data.name, layoutId: response.data.layouts[0].id, slug: response.data.slug, customSlug: response.data.customSlug, userPermissions: response.data.userPermissions, }, }); // Add this to the page DSLs for entity explorer // The dslTransformer may not be necessary for the entity explorer // However, we still transform for consistency. const currentDSL: { dsl: DSLWidget; layoutId: string | undefined } = yield extractCurrentDSL({ dslTransformer, response, }); yield put({ type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, payload: { pageId: response.data.id, dsl: currentDSL.dsl, layoutId: response.data.layouts[0].id, }, }); // TODO: Update URL params here // route to generate template for new page created history.push( builderURL({ basePageId: response.data.baseId, }), ); } } catch (error) { yield put({ type: ReduxActionErrorTypes.CREATE_PAGE_ERROR, payload: { error, }, }); } } export function* updatePageSaga(action: ReduxAction) { const params = { ...action.payload, pageId: action.payload.id }; try { params.customSlug = params.customSlug?.replaceAll(" ", "-"); const response: ApiResponse = yield call( PageApi.updatePage, params, ); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { yield put(updatePageSuccess(response.data)); } } catch (error) { yield put( updatePageError({ request: params, error, }), ); } } export function* deletePageSaga(action: ReduxAction) { try { const { id: pageId } = action.payload; const defaultPageId: string = yield select(getDefaultPageId); const defaultBasePageId: string = yield select(getDefaultBasePageId); const currentPageId: string = yield select(getCurrentPageId); if (defaultPageId === pageId) { throw Error("Cannot delete the home page."); } else { const params = { pageId: pageId }; const response: ApiResponse = yield call(PageApi.deletePage, params); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { yield put(deletePageSuccess()); } // Remove this page from page DSLs yield put({ type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, payload: { pageId, dsl: undefined, }, }); // Update route params here if (currentPageId === pageId) history.push( builderURL({ basePageId: defaultBasePageId, }), ); } } catch (error) { yield put({ type: ReduxActionErrorTypes.DELETE_PAGE_ERROR, payload: { error: { message: (error as Error).message, show: true }, show: true, }, }); } } export function* clonePageSaga( clonePageAction: ReduxAction, ) { try { const request = { pageId: clonePageAction.payload.id, }; const response: FetchPageResponse = yield call(PageApi.clonePage, request); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { yield put( clonePageSuccess({ pageId: response.data.id, basePageId: response.data.baseId, pageName: response.data.name, layoutId: response.data.layouts[0].id, slug: response.data.slug, isDefault: false, }), ); // Add this to the page DSLs for entity explorer // We're not sending the `dslTransformer` to the `extractCurrentDSL` function // as this is a clone operation, and any layout system specific // updates to the DSL would have already been performed in the original page const { dsl, layoutId } = yield extractCurrentDSL({ response, }); yield put({ type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, payload: { pageId: response.data.id, dsl, layoutId, }, }); const triggersAfterPageFetch = [ fetchActionsForPage(response.data.id), fetchJSCollectionsForPage(response.data.id), ]; const afterActionsFetch: unknown = yield failFastApiCalls( triggersAfterPageFetch, [ fetchActionsForPageSuccess([]).type, fetchJSCollectionsForPageSuccess([]).type, ], [ fetchActionsForPageError().type, fetchJSCollectionsForPageError().type, ], ); if (!afterActionsFetch) { throw new Error("Failed cloning page"); } yield put(selectWidgetInitAction(SelectionRequestType.Empty)); yield put( fetchAllPageEntityCompletion([ executePageLoadActions(ActionExecutionContext.CLONE_PAGE), ]), ); // TODO: Update URL params here. if (!clonePageAction.payload.blockNavigation) { history.push( builderURL({ basePageId: response.data.baseId, }), ); } } } catch (error) { yield put({ type: ReduxActionErrorTypes.CLONE_PAGE_ERROR, payload: { error, }, }); } } export class WidgetNameUpdateExtension { // Singleton instance private static instance = new WidgetNameUpdateExtension(); // The extension function storage private extensionFunction: | ((params: HandleWidgetNameUpdatePayload) => Generator) | null = null; // Private constructor private constructor() {} // Get the instance static getInstance() { return this.instance; } // Set the extension function setExtension(fn: (params: HandleWidgetNameUpdatePayload) => Generator) { this.extensionFunction = fn; } // Get the extension function getExtension() { return this.extensionFunction; } } export function* updateWidgetNameAPISaga( requestParams: UpdateWidgetNameRequest, ) { const response: UpdateWidgetNameResponse = yield call( PageApi.updateWidgetName, requestParams, ); const isValidResponse: boolean = yield validateResponse(response); return { response, isValidResponse }; } export function* handleWidgetNameUpdateDefault( params: HandleWidgetNameUpdatePayload, ) { const { newName, widgetName } = params; const layoutId: string | undefined = yield select(getCurrentLayoutId); const pageId: string | undefined = yield select(getCurrentPageId); const request: UpdateWidgetNameRequest = { newName: newName, oldName: widgetName, pageId, // @ts-expect-error: layoutId can be undefined layoutId, }; const { isValidResponse, response } = yield call( updateWidgetNameAPISaga, request, ); if (isValidResponse) { // @ts-expect-error: pageId can be undefined yield updateCanvasWithDSL(response.data, pageId, layoutId); yield put(updateWidgetNameSuccess()); // Add this to the page DSLs for entity explorer yield put({ type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS, payload: { pageId: pageId, dsl: response.data.dsl, layoutId, }, }); checkAndLogErrorsIfCyclicDependency( (response.data as PageLayout).layoutOnLoadActionErrors, ); } } export function* handleWidgetNameUpdate(params: HandleWidgetNameUpdatePayload) { const extension = WidgetNameUpdateExtension.getInstance().getExtension(); if (extension) { yield call(extension, params); } else { yield call(handleWidgetNameUpdateDefault, params); } } /** * this saga do two things * * 1. Checks if the name of page is conflicting with any used name * 2. dispatches a action which triggers a request to update the name * * @param action */ export function* updateWidgetNameSaga( action: ReduxAction<{ id: string; newName: string }>, ) { try { const { widgetName } = yield select(getWidgetName, action.payload.id); const getUsedNames: Record = yield select( getUsedActionNames, "", ); // TODO(abhinav): Why do we need to jump through these hoops just to // change the tab name? Figure out a better design to make this moot. const tabsObj: Record< string, { id: string; widgetId: string; label: string; } > = yield select((state: DefaultRootState) => { // Check if this widget exists in the canvas widgets if (state.entities.canvasWidgets.hasOwnProperty(action.payload.id)) { // If it does assign it to a variable const widget = state.entities.canvasWidgets[action.payload.id]; // Check if this widget has a parent in the canvas widgets if ( widget.parentId && state.entities.canvasWidgets.hasOwnProperty(widget.parentId) ) { // If the parent exists assign it to a variable const parent = state.entities.canvasWidgets[widget.parentId]; // Check if this parent is a TABS_WIDGET if (parent.type === WidgetTypes.TABS_WIDGET) { // If it is return the tabs property return parent.tabsObj; } } } // This isn't a tab in a tabs widget so return undefined return; }); // If we're trying to update the name of a tab in the TABS_WIDGET if (tabsObj !== undefined) { // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any const tabs: any = Object.values(tabsObj); // Get all canvas widgets const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets); // Shallow copy canvas widgets as they're immutable const widgets = { ...stateWidgets }; // Get the parent Id of the tab (canvas widget) whose name we're updating const parentId = widgets[action.payload.id].parentId; // Update the tabName property of the tab (canvas widget) widgets[action.payload.id] = { ...widgets[action.payload.id], tabName: action.payload.newName, }; // Shallow copy the parent widget so that we can update the properties // @ts-expect-error parentId can be undefined const parent = { ...widgets[parentId] }; // Update the tabs property of the parent tabs widget const tabToChange = tabs.find( // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any (each: any) => each.widgetId === action.payload.id, ); const updatedTab = { ...tabToChange, label: action.payload.newName, }; parent.tabsObj = { ...parent.tabsObj, [updatedTab.id]: { ...updatedTab, }, }; // replace the parent widget in the canvas widgets // @ts-expect-error parentId can be undefined widgets[parentId] = parent; // Update and save the new widgets //TODO Identify the updated widgets and pass the values yield put(updateAndSaveLayout(widgets)); // Send a update saying that we've successfully updated the name yield put(updateWidgetNameSuccess()); } else { // check if name is not conflicting with any // existing entity/api/queries/reserved words if (isNameValid(action.payload.newName, getUsedNames)) { yield call(handleWidgetNameUpdate, { newName: action.payload.newName, widgetName, }); } else { yield put({ type: ReduxActionErrorTypes.UPDATE_WIDGET_NAME_ERROR, payload: { error: { message: `Entity name: ${action.payload.newName} is already being used or is a restricted keyword.`, }, }, }); } } } catch (error) { yield put({ type: ReduxActionErrorTypes.UPDATE_WIDGET_NAME_ERROR, payload: { error, }, }); } } export function* updateCanvasWithDSL( data: PageLayout & { dsl: WidgetProps }, pageId: string, layoutId: string, ) { const flattenedDSL = flattenDSL(data.dsl); const currentPageName: string = yield select(getCurrentPageName); const applicationId: string = yield select(getCurrentApplicationId); const pageWidgetId = MAIN_CONTAINER_WIDGET_ID; const canvasWidgetsPayload: UpdateCanvasPayload = { pageWidgetId, currentPageName, currentPageId: pageId, currentLayoutId: layoutId, currentApplicationId: applicationId, dsl: data.dsl, pageActions: data.layoutOnLoadActions, widgets: flattenedDSL, }; yield put(initCanvasLayout(canvasWidgetsPayload)); yield put(fetchActionsForPage(pageId)); yield put(fetchJSCollectionsForPage(pageId)); } export function* setDataUrl() { const urlData: UrlDataState = { fullPath: window.location.href, host: window.location.host, hostname: window.location.hostname, queryParams: getQueryParams(), protocol: window.location.protocol, pathname: window.location.pathname, port: window.location.port, hash: window.location.hash, }; yield put(setUrlData(urlData)); } export function* fetchPageDSLSaga( pageId: string, pageDSL?: ApiResponse, ) { try { const layoutSystemType: LayoutSystemTypes = yield select(getLayoutSystemType); const mainCanvasProps: MainCanvasReduxState = yield select(getMainCanvasProps); const dslTransformer = getLayoutSystemDSLTransformer( layoutSystemType, mainCanvasProps.width, ); const params: FetchPageRequest = { pageId, migrateDSL: true }; const fetchPageResponse: FetchPageResponse = yield call( getFromServerWhenNoPrefetchedResult, pageDSL, () => call(PageApi.fetchPage, params), ); const isValidResponse: boolean = yield validateResponse(fetchPageResponse); if (isValidResponse) { // Wait for the Widget config to be loaded before we can migrate the DSL yield call(waitForWidgetConfigBuild); // DSL migrations will now happen on the server // So, it may not be necessary to run dslTransformer on the pageDSL // or to run the DSL by the extractCurrentDSL function // Another caveat to note is that we have conversions happening // between Auto Layout and Fixed layout systems, this means that // particularly for these two layout systems the dslTransformer may be necessary // unless we're no longer running any conversions const { dsl, layoutId } = yield extractCurrentDSL({ dslTransformer, response: fetchPageResponse, }); return { pageId, dsl, layoutId, userPermissions: fetchPageResponse.data?.userPermissions, }; } } catch (error) { yield put({ type: ReduxActionErrorTypes.FETCH_PAGE_DSL_ERROR, payload: { pageId: pageId, error, show: true, }, }); return { pageId: pageId, dsl: DEFAULT_TEMPLATE, }; } } export function* populatePageDSLsSaga(action?: { payload?: { pagesWithMigratedDsl?: ApiResponse }; }) { const { pagesWithMigratedDsl } = action?.payload || {}; try { const pageIds: string[] = yield select((state: DefaultRootState) => state.entities.pageList.pages.map((page: Page) => page.pageId), ); const pageDSLs: unknown = yield all( pageIds.map((pageId: string) => { if (!pagesWithMigratedDsl) { return call(fetchPageDSLSaga, pageId); } const { data } = pagesWithMigratedDsl; // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any const v1PageDSL = data?.find?.((v: any) => v?.id === pageId); return call(fetchPageDSLSaga, pageId, { ...pagesWithMigratedDsl, data: v1PageDSL, } as ApiResponse); }), ); yield put({ type: ReduxActionTypes.FETCH_PAGE_DSLS_SUCCESS, payload: pageDSLs, }); yield put({ type: ReduxActionTypes.UPDATE_PAGE_LIST, payload: pageDSLs, }); } catch (error) { yield put({ type: ReduxActionErrorTypes.POPULATE_PAGEDSLS_ERROR, payload: { error, }, }); } } export function* setPageOrderSaga( action: ReduxAction, ) { try { const request = { applicationId: action.payload.applicationId, pageId: action.payload.pageId, order: action.payload.order, }; const response: ApiResponse = yield call(PageApi.setPageOrder, request); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { yield put({ type: ReduxActionTypes.SET_PAGE_ORDER_SUCCESS, payload: { // @ts-expect-error: response.data is of type unknown pages: response.data.pages, }, }); } } catch (error) { yield put({ type: ReduxActionErrorTypes.SET_PAGE_ORDER_ERROR, payload: { error, }, }); } } export function* generateTemplatePageSaga( action: ReduxAction, ) { try { const request: GenerateTemplatePageRequest = action.payload; // if pageId is available in request, it will just update that page else it will generate new page. const response: ApiResponse<{ // TODO: Fix this the next time the file is edited // eslint-disable-next-line @typescript-eslint/no-explicit-any page: any; successImageUrl: string; successMessage: string; }> = yield call(PageApi.generateTemplatePage, request); const isValidResponse: boolean = yield validateResponse(response); if (isValidResponse) { const pageId = response.data.page.id; yield put( generateTemplateSuccess({ page: response.data.page, isNewPage: !request.pageId, // if pageId if not defined, that means a new page is generated. }), ); yield handleFetchedPage({ fetchPageResponse: { data: response.data.page, responseMeta: response.responseMeta, }, pageId, isFirstLoad: true, }); yield put(fetchPageAction(pageId)); // trigger evaluation after completion of page success & fetch actions for page + fetch jsobject for page const triggersAfterPageFetch = [ fetchActionsForPage(pageId), fetchJSCollectionsForPage(pageId), ]; const afterActionsFetch: unknown = yield failFastApiCalls( triggersAfterPageFetch, [ fetchActionsForPageSuccess([]).type, fetchJSCollectionsForPageSuccess([]).type, ], [ fetchActionsForPageError().type, fetchJSCollectionsForPageError().type, ], ); if (!afterActionsFetch) { throw new Error("Failed generating template"); } yield put( fetchAllPageEntityCompletion([ executePageLoadActions(ActionExecutionContext.GENERATE_CRUD_PAGE), ]), ); const basePageId: string = yield select( convertToBasePageIdSelector, pageId, ); history.replace( builderURL({ basePageId, }), ); // TODO : Add it to onSuccessCallback toast.show("Successfully generated a page", { kind: "success", }); yield put( setCrudInfoModalData({ open: true, generateCRUDSuccessInfo: { successImageUrl: response.data.successImageUrl, successMessage: response.data.successMessage, }, }), ); } } catch (error) { yield put(generateTemplateError()); } } export function* deleteCanvasCardsStateSaga() { const currentPageId: string = yield select(getCurrentPageId); const state = JSON.parse( localStorage.getItem(LOCAL_STORAGE_KEYS.CANVAS_CARDS_STATE) ?? "{}", ); delete state[currentPageId]; localStorage.setItem( LOCAL_STORAGE_KEYS.CANVAS_CARDS_STATE, JSON.stringify(state), ); } export function* setCanvasCardsStateSaga(action: ReduxAction) { const state = localStorage.getItem(LOCAL_STORAGE_KEYS.CANVAS_CARDS_STATE); const stateObject = JSON.parse(state ?? "{}"); stateObject[action.payload] = true; localStorage.setItem( LOCAL_STORAGE_KEYS.CANVAS_CARDS_STATE, JSON.stringify(stateObject), ); } export function* setPreviewModeInitSaga(action: ReduxAction) { const isPreviewMode: boolean = yield select(selectCombinedPreviewMode); if (action.payload) { // we animate out elements and then move to the canvas yield put(setPreviewModeAction(action.payload)); } else if (isPreviewMode) { // check if already in edit mode, then only do this yield put(setPreviewModeAction(action.payload)); } } export function* setupPageSaga(action: ReduxAction) { try { const { id: pageId, isFirstLoad = false, pageWithMigratedDsl, } = action.payload; /* Added the first line for isPageSwitching redux state to be true when page is being fetched to fix scroll position issue. Added the second line for sync call instead of async (due to first line) as it was leading to issue with on page load actions trigger. */ yield put(fetchPageAction(pageId, isFirstLoad, pageWithMigratedDsl)); yield take(ReduxActionTypes.FETCH_PAGE_SUCCESS); yield put({ type: ReduxActionTypes.SETUP_PAGE_SUCCESS, }); } catch (error) { yield put({ type: ReduxActionErrorTypes.SETUP_PAGE_ERROR, payload: { error }, }); } } export function* setupPublishedPageSaga( action: ReduxAction, ) { try { const { bustCache, pageId, pageWithMigratedDsl } = action.payload; /* Added the first line for isPageSwitching redux state to be true when page is being fetched to fix scroll position issue. Added the second line for sync call instead of async (due to first line) as it was leading to issue with on page load actions trigger. */ yield put(fetchPublishedPageAction(pageId, bustCache, pageWithMigratedDsl)); yield take(ReduxActionTypes.FETCH_PUBLISHED_PAGE_SUCCESS); yield put({ type: ReduxActionTypes.SETUP_PUBLISHED_PAGE_SUCCESS, }); } catch (error) { yield put({ type: ReduxActionErrorTypes.SETUP_PUBLISHED_PAGE_ERROR, payload: { error }, }); } }