PromucFlow_constructor/app/client/src/sagas/PageSagas.tsx
2022-08-04 11:10:44 +05:30

1131 lines
33 KiB
TypeScript

import CanvasWidgetsNormalizer from "normalizers/CanvasWidgetsNormalizer";
import { AppState } from "reducers";
import {
Page,
ReduxAction,
ReduxActionErrorTypes,
ReduxActionTypes,
UpdateCanvasPayload,
} from "@appsmith/constants/ReduxActionConstants";
import {
clonePageSuccess,
deletePageSuccess,
FetchPageListPayload,
fetchPageSuccess,
fetchPublishedPageSuccess,
savePageSuccess,
setUrlData,
initCanvasLayout,
updateCurrentPage,
updateWidgetNameSuccess,
updateAndSaveLayout,
saveLayout,
setLastUpdatedTime,
ClonePageActionPayload,
CreatePageActionPayload,
generateTemplateError,
generateTemplateSuccess,
fetchAllPageEntityCompletion,
} from "actions/pageActions";
import PageApi, {
ClonePageRequest,
CreatePageRequest,
DeletePageRequest,
FetchPageListResponse,
FetchPageRequest,
FetchPageResponse,
FetchPublishedPageRequest,
PageLayout,
SavePageResponse,
SetPageOrderRequest,
UpdatePageRequest,
UpdateWidgetNameRequest,
UpdateWidgetNameResponse,
} from "api/PageApi";
import {
CanvasWidgetsReduxState,
FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
all,
call,
debounce,
put,
select,
takeLatest,
takeLeading,
} from "redux-saga/effects";
import history from "utils/history";
import { captureInvalidDynamicBindingPath, isNameValid } from "utils/helpers";
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
import { checkIfMigrationIsNeeded } from "utils/DSLMigrations";
import {
getAllPageIds,
getEditorConfigs,
getExistingPageNames,
getWidgets,
} from "./selectors";
import { getDataTree } from "selectors/dataTreeSelectors";
import { IncorrectBindingError, validateResponse } from "./ErrorSagas";
import { ApiResponse } from "api/ApiResponses";
import {
getCurrentApplicationId,
getCurrentLayoutId,
getCurrentPageId,
getCurrentPageName,
} from "selectors/editorSelectors";
import {
executePageLoadActions,
fetchActionsForPage,
setActionsToExecuteOnPageLoad,
setJSActionsToExecuteOnPageLoad,
fetchActionsForPageSuccess,
fetchActionsForPageError,
} from "actions/pluginActionActions";
import { UrlDataState } from "reducers/entityReducers/appReducer";
import { APP_MODE } from "entities/App";
import { clearEvalCache } from "./EvaluationsSaga";
import { getQueryParams } from "utils/URLUtils";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import log from "loglevel";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import { migrateIncorrectDynamicBindingPathLists } from "utils/migrations/IncorrectDynamicBindingPathLists";
import * as Sentry from "@sentry/react";
import { ERROR_CODES } from "@appsmith/constants/ApiConstants";
import AnalyticsUtil from "utils/AnalyticsUtil";
import DEFAULT_TEMPLATE from "templates/default";
import { GenerateTemplatePageRequest } from "api/PageApi";
import { getAppMode } from "selectors/applicationSelectors";
import { setCrudInfoModalData } from "actions/crudInfoModalActions";
import { selectMultipleWidgetsAction } from "actions/widgetSelectionActions";
import {
getIsFirstTimeUserOnboardingEnabled,
getFirstTimeUserOnboardingApplicationId,
inGuidedTour,
} from "selectors/onboardingSelectors";
import {
fetchJSCollectionsForPage,
fetchJSCollectionsForPageSuccess,
fetchJSCollectionsForPageError,
} from "actions/jsActionActions";
import WidgetFactory from "utils/WidgetFactory";
import { toggleShowDeviationDialog } from "actions/onboardingActions";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { builderURL, generateTemplateURL } from "RouteBuilder";
import { failFastApiCalls } from "./InitSagas";
import { takeEvery } from "redux-saga/effects";
const WidgetTypes = WidgetFactory.widgetTypes;
const getWidgetName = (state: AppState, widgetId: string) =>
state.entities.canvasWidgets[widgetId];
export function* fetchPageListSaga(
fetchPageListAction: ReduxAction<FetchPageListPayload>,
) {
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_LIST_API,
);
try {
const { applicationId, mode } = fetchPageListAction.payload;
const apiCall =
mode === APP_MODE.EDIT
? PageApi.fetchPageList
: PageApi.fetchPageListViewMode;
const response: FetchPageListResponse = yield call(apiCall, applicationId);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
const workspaceId = response.data.workspaceId;
const pages: Page[] = response.data.pages.map((page) => ({
pageName: page.name,
pageId: page.id,
isDefault: page.isDefault,
isHidden: !!page.isHidden,
slug: page.slug,
}));
yield put({
type: ReduxActionTypes.SET_CURRENT_WORKSPACE_ID,
payload: {
workspaceId,
},
});
yield put({
type: ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS,
payload: {
pages,
applicationId: applicationId,
},
});
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_LIST_API,
);
} else {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_LIST_API,
);
yield put({
type: ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR,
payload: {
error: response.responseMeta.error,
},
});
}
} catch (error) {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_LIST_API,
{ failed: true },
);
yield put({
type: ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR,
payload: {
error,
},
});
}
}
export const getCanvasWidgetsPayload = (
pageResponse: FetchPageResponse,
): UpdateCanvasPayload => {
const normalizedResponse = CanvasWidgetsNormalizer.normalize(
extractCurrentDSL(pageResponse),
);
return {
pageWidgetId: normalizedResponse.result,
currentPageName: pageResponse.data.name,
currentPageId: pageResponse.data.id,
widgets: normalizedResponse.entities.canvasWidgets,
currentLayoutId: pageResponse.data.layouts[0].id, // TODO(abhinav): Handle for multiple layouts
currentApplicationId: pageResponse.data.applicationId,
pageActions: pageResponse.data.layouts[0].layoutOnLoadActions || [],
};
};
export function* handleFetchedPage({
fetchPageResponse,
isFirstLoad = false,
pageId,
}: {
fetchPageResponse: FetchPageResponse;
pageId: string;
isFirstLoad?: boolean;
}) {
const isValidResponse: boolean = yield validateResponse(fetchPageResponse);
const willPageBeMigrated = checkIfMigrationIsNeeded(fetchPageResponse);
const lastUpdatedTime = getLastUpdateTime(fetchPageResponse);
const pageSlug = fetchPageResponse.data.slug;
if (isValidResponse) {
// Clear any existing caches
yield call(clearEvalCache);
// Set url params
yield call(setDataUrl);
// Get Canvas payload
const canvasWidgetsPayload = getCanvasWidgetsPayload(fetchPageResponse);
// Update the canvas
yield put(initCanvasLayout(canvasWidgetsPayload));
// set current page
yield put(updateCurrentPage(pageId, pageSlug));
// 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));
const extractedDSL = extractCurrentDSL(fetchPageResponse);
yield put({
type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE,
payload: extractedDSL,
});
if (willPageBeMigrated) {
yield put(saveLayout());
}
}
}
const getLastUpdateTime = (pageResponse: FetchPageResponse): number =>
pageResponse.data.lastUpdatedTime;
export function* fetchPageSaga(
pageRequestAction: ReduxAction<FetchPageRequest>,
) {
try {
const { id, isFirstLoad } = pageRequestAction.payload;
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
{ pageId: id },
);
const fetchPageResponse: FetchPageResponse = yield call(PageApi.fetchPage, {
id,
});
yield handleFetchedPage({
fetchPageResponse,
pageId: id,
isFirstLoad,
});
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
);
} catch (error) {
log.error(error);
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
{
failed: true,
},
);
yield put({
type: ReduxActionErrorTypes.FETCH_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* fetchPublishedPageSaga(
pageRequestAction: ReduxAction<{
pageId: string;
bustCache: boolean;
firstLoad: boolean;
}>,
) {
try {
const { bustCache, firstLoad, pageId } = pageRequestAction.payload;
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
{
pageId: pageId,
published: true,
},
);
const request: FetchPublishedPageRequest = {
pageId,
bustCache,
};
const response: FetchPageResponse = yield call(
PageApi.fetchPublishedPage,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
// Clear any existing caches
yield call(clearEvalCache);
// Set url params
yield call(setDataUrl);
// Get Canvas payload
const canvasWidgetsPayload = getCanvasWidgetsPayload(response);
// Update the canvas
yield put(initCanvasLayout(canvasWidgetsPayload));
// set current page
yield put(updateCurrentPage(pageId, response.data.slug));
// dispatch fetch page success
yield put(fetchPublishedPageSuccess());
/* 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 (!firstLoad) {
yield put(fetchAllPageEntityCompletion([executePageLoadActions()]));
}
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
);
}
} catch (error) {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.FETCH_PAGE_API,
{
failed: true,
},
);
yield put({
type: ReduxActionErrorTypes.FETCH_PUBLISHED_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* fetchAllPublishedPagesSaga() {
try {
const pageIds: string[] = yield select(getAllPageIds);
yield all(
pageIds.map((pageId: string) => {
return call(PageApi.fetchPublishedPage, { pageId, bustCache: true });
}),
);
} catch (error) {
log.error({ error });
}
}
function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) {
const widgets: CanvasWidgetsReduxState = yield select(getWidgets);
const editorConfigs:
| {
pageId: string;
layoutId: string;
}
| undefined = yield select(getEditorConfigs) as any;
const guidedTourEnabled: boolean = yield select(inGuidedTour);
const savePageRequest = getLayoutSavePayload(widgets, editorConfigs);
PerformanceTracker.startAsyncTracking(
PerformanceTransactionName.SAVE_PAGE_API,
{
pageId: savePageRequest.pageId,
},
);
try {
// Store the updated DSL in the pageDSLs reducer
yield put({
type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS,
payload: {
pageId: savePageRequest.pageId,
dsl: savePageRequest.dsl,
},
});
yield put({
type: ReduxActionTypes.UPDATE_CANVAS_STRUCTURE,
payload: savePageRequest.dsl,
});
captureInvalidDynamicBindingPath(
CanvasWidgetsNormalizer.denormalize("0", {
canvasWidgets: 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 && !guidedTourEnabled) {
savePageResponse.data.messages.forEach((message) => {
Toaster.show({
text: message,
type: Variant.info,
});
});
}
// Update actions
if (actionUpdates && actionUpdates.length > 0) {
const actions = actionUpdates.filter(
(d) => !d.hasOwnProperty("collectionId"),
);
if (actions && actions.length) {
yield put(setActionsToExecuteOnPageLoad(actions));
}
const jsActions = actionUpdates.filter((d) =>
d.hasOwnProperty("collectionId"),
);
if (jsActions && jsActions.length) {
yield put(setJSActionsToExecuteOnPageLoad(jsActions));
}
}
yield put(setLastUpdatedTime(Date.now() / 1000));
yield put(savePageSuccess(savePageResponse));
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.SAVE_PAGE_API,
);
}
} catch (error) {
PerformanceTracker.stopAsyncTracking(
PerformanceTransactionName.SAVE_PAGE_API,
{
failed: true,
},
);
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) {
Sentry.captureException(new Error("Failed to correct binding paths"));
yield put({
type: ReduxActionErrorTypes.FAILED_CORRECTING_BINDING_PATHS,
payload: {
error: {
message,
code: ERROR_CODES.FAILED_TO_CORRECT_BINDING,
crash: true,
},
},
});
} else {
// Create a denormalized structure because the migration needs the children in the dsl form
const denormalizedWidgets = CanvasWidgetsNormalizer.denormalize("0", {
canvasWidgets: widgets,
});
const correctedWidgets = migrateIncorrectDynamicBindingPathLists(
denormalizedWidgets,
);
// Normalize the widgets because the save page needs it in the flat structure
const normalizedWidgets = CanvasWidgetsNormalizer.normalize(
correctedWidgets,
);
AnalyticsUtil.logEvent("CORRECT_BAD_BINDING", {
error: error.message,
correctWidget: JSON.stringify(normalizedWidgets),
});
yield put(
updateAndSaveLayout(normalizedWidgets.entities.canvasWidgets, true),
);
}
}
}
}
function getLayoutSavePayload(
widgets: {
[widgetId: string]: FlattenedWidgetProps;
},
editorConfigs: any,
) {
const denormalizedDSL = CanvasWidgetsNormalizer.denormalize(
Object.keys(widgets)[0],
{ canvasWidgets: widgets },
);
return {
...editorConfigs,
dsl: denormalizedDSL,
};
}
export function* saveLayoutSaga(action: ReduxAction<{ isRetry?: boolean }>) {
try {
const appMode: APP_MODE | undefined = yield select(getAppMode);
if (appMode === APP_MODE.EDIT) {
yield put(saveLayout(action.payload.isRetry));
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.SAVE_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* createPageSaga(
createPageAction: ReduxAction<CreatePageActionPayload>,
) {
try {
const guidedTourEnabled: boolean = yield select(inGuidedTour);
// Prevent user from creating a new page during the guided tour
if (guidedTourEnabled) {
yield put(toggleShowDeviationDialog(true));
return;
}
const request: CreatePageRequest = createPageAction.payload;
const response: FetchPageResponse = yield call(PageApi.createPage, request);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.CREATE_PAGE_SUCCESS,
payload: {
pageId: response.data.id,
pageName: response.data.name,
layoutId: response.data.layouts[0].id,
slug: response.data.slug,
customSlug: response.data.customSlug,
},
});
// Add this to the page DSLs for entity explorer
yield put({
type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS,
payload: {
pageId: response.data.id,
dsl: extractCurrentDSL(response),
},
});
// TODO: Update URL params here
// route to generate template for new page created
if (!createPageAction.payload.blockNavigation) {
const firstTimeUserOnboardingApplicationId: string = yield select(
getFirstTimeUserOnboardingApplicationId,
);
const isFirstTimeUserOnboardingEnabled: boolean = yield select(
getIsFirstTimeUserOnboardingEnabled,
);
if (
firstTimeUserOnboardingApplicationId ==
createPageAction.payload.applicationId &&
isFirstTimeUserOnboardingEnabled
) {
history.push(
builderURL({
pageId: response.data.id,
}),
);
} else {
history.push(
generateTemplateURL({
pageId: response.data.id,
}),
);
}
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CREATE_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* updatePageSaga(action: ReduxAction<UpdatePageRequest>) {
try {
const request: UpdatePageRequest = action.payload;
const response: ApiResponse = yield call(PageApi.updatePage, request);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.UPDATE_PAGE_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPDATE_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* deletePageSaga(action: ReduxAction<DeletePageRequest>) {
try {
const request: DeletePageRequest = action.payload;
const defaultPageId: string = yield select(
(state: AppState) => state.entities.pageList.defaultPageId,
);
if (defaultPageId === request.id) {
throw Error("Cannot delete the home page.");
} else {
const response: ApiResponse = yield call(PageApi.deletePage, request);
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: request.id,
dsl: undefined,
},
});
// Update route params here
const currentPageId: string = yield select(
(state: AppState) => state.entities.pageList.currentPageId,
);
if (currentPageId === action.payload.id)
history.push(
builderURL({
pageId: defaultPageId,
}),
);
}
} 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<ClonePageActionPayload>,
) {
try {
const request: ClonePageRequest = clonePageAction.payload;
const response: FetchPageResponse = yield call(PageApi.clonePage, request);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put(
clonePageSuccess(
response.data.id,
response.data.name,
response.data.layouts[0].id,
response.data.slug,
),
);
// Add this to the page DSLs for entity explorer
yield put({
type: ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS,
payload: {
pageId: response.data.id,
dsl: extractCurrentDSL(response),
},
});
yield put(fetchActionsForPage(response.data.id));
yield put(fetchJSCollectionsForPage(response.data.id));
yield put(selectMultipleWidgetsAction([]));
// TODO: Update URL params here.
if (!clonePageAction.payload.blockNavigation) {
history.push(
builderURL({
pageId: response.data.id,
}),
);
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CLONE_PAGE_ERROR,
payload: {
error,
},
});
}
}
/**
* 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 layoutId: string | undefined = yield select(getCurrentLayoutId);
const evalTree: DataTree = yield select(getDataTree);
const pageId: string | undefined = yield select(getCurrentPageId);
const existingPageNames: Record<string, unknown> = yield select(
getExistingPageNames,
);
// 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: AppState) => {
// Check if this widget exists in the canvas widgets
if (state.entities.canvasWidgets.hasOwnProperty(action.payload.id)) {
// If it does assign it to a variable
const widget = state.entities.canvasWidgets[action.payload.id];
// Check if this widget has a parent in the canvas widgets
if (
widget.parentId &&
state.entities.canvasWidgets.hasOwnProperty(widget.parentId)
) {
// If the parent exists assign it to a variable
const parent = state.entities.canvasWidgets[widget.parentId];
// Check if this parent is a TABS_WIDGET
if (parent.type === WidgetTypes.TABS_WIDGET) {
// If it is return the tabs property
return parent.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) {
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(
(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
yield put(updateAndSaveLayout(widgets));
// Send a update saying that we've successfully updated the name
yield put(updateWidgetNameSuccess());
} else {
// check if name is not conflicting with any
// existing entity/api/queries/reserved words
if (
isNameValid(action.payload.newName, {
...evalTree,
...existingPageNames,
})
) {
const request: UpdateWidgetNameRequest = {
newName: action.payload.newName,
oldName: widgetName,
// @ts-expect-error: pageId can be undefined
pageId,
// @ts-expect-error: layoutId can be undefined
layoutId,
};
const response: UpdateWidgetNameResponse = yield call(
PageApi.updateWidgetName,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
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,
},
});
}
} 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,
pageId: string,
layoutId: string,
) {
const normalizedWidgets = CanvasWidgetsNormalizer.normalize(data.dsl);
const currentPageName: string = yield select(getCurrentPageName);
const applicationId: string = yield select(getCurrentApplicationId);
const canvasWidgetsPayload: UpdateCanvasPayload = {
pageWidgetId: normalizedWidgets.result,
currentPageName,
currentPageId: pageId,
currentLayoutId: layoutId,
currentApplicationId: applicationId,
pageActions: data.layoutOnLoadActions,
widgets: normalizedWidgets.entities.canvasWidgets,
};
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));
}
function* fetchPageDSLSaga(pageId: string) {
try {
const fetchPageResponse: FetchPageResponse = yield call(PageApi.fetchPage, {
id: pageId,
});
const isValidResponse: boolean = yield validateResponse(fetchPageResponse);
if (isValidResponse) {
return {
pageId: pageId,
dsl: extractCurrentDSL(fetchPageResponse),
};
}
} 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() {
try {
const pageIds: string[] = yield select((state: AppState) =>
state.entities.pageList.pages.map((page: Page) => page.pageId),
);
const pageDSLs: unknown = yield all(
pageIds.map((pageId: string) => {
return call(fetchPageDSLSaga, pageId);
}),
);
yield put({
type: ReduxActionTypes.FETCH_PAGE_DSLS_SUCCESS,
payload: pageDSLs,
});
} catch (error) {
yield put({
type: ReduxActionErrorTypes.POPULATE_PAGEDSLS_ERROR,
payload: {
error,
},
});
}
}
/**
* saga to update the page order
*
* @param action
*/
export function* setPageOrderSaga(action: ReduxAction<SetPageOrderRequest>) {
try {
const request: SetPageOrderRequest = action.payload;
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,
},
});
}
}
function* setCustomSlugSaga(
action: ReduxAction<{ pageId: string; customSlug: string }>,
) {
const { customSlug, pageId } = action.payload;
const response: ApiResponse<Page> = yield call(PageApi.updatePage, {
id: pageId,
customSlug,
});
try {
const isValidResponse: boolean = yield validateResponse(response);
if (!isValidResponse) return;
yield put({
type: ReduxActionTypes.UPDATE_PAGE_SUCCESS,
payload: response.data,
});
yield put({
type: ReduxActionTypes.UPDATE_CUSTOM_SLUG_SUCCESS,
payload: {
pageId,
},
});
} catch (e) {
yield put({
type: ReduxActionErrorTypes.UPDATE_CUSTOM_SLUG_ERROR,
payload: {
pageId,
},
});
}
}
export function* generateTemplatePageSaga(
action: ReduxAction<GenerateTemplatePageRequest>,
) {
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<{
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,
});
// 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()]));
history.replace(
builderURL({
pageId,
}),
);
// TODO : Add it to onSuccessCallback
Toaster.show({
text: "Successfully generated a page",
variant: Variant.success,
});
yield put(
setCrudInfoModalData({
open: true,
generateCRUDSuccessInfo: {
successImageUrl: response.data.successImageUrl,
successMessage: response.data.successMessage,
},
}),
);
}
} catch (error) {
yield put(generateTemplateError());
}
}
export default function* pageSagas() {
yield all([
takeLatest(ReduxActionTypes.FETCH_PAGE_INIT, fetchPageSaga),
takeLatest(
ReduxActionTypes.FETCH_PUBLISHED_PAGE_INIT,
fetchPublishedPageSaga,
),
takeLatest(ReduxActionTypes.UPDATE_LAYOUT, saveLayoutSaga),
takeLeading(ReduxActionTypes.CREATE_PAGE_INIT, createPageSaga),
takeLeading(ReduxActionTypes.CLONE_PAGE_INIT, clonePageSaga),
takeLatest(ReduxActionTypes.UPDATE_PAGE_INIT, updatePageSaga),
takeLatest(ReduxActionTypes.DELETE_PAGE_INIT, deletePageSaga),
debounce(500, ReduxActionTypes.SAVE_PAGE_INIT, savePageSaga),
takeLatest(ReduxActionTypes.UPDATE_WIDGET_NAME_INIT, updateWidgetNameSaga),
takeLatest(
ReduxActionTypes.FETCH_ALL_PUBLISHED_PAGES,
fetchAllPublishedPagesSaga,
),
takeLatest(
ReduxActionTypes.GENERATE_TEMPLATE_PAGE_INIT,
generateTemplatePageSaga,
),
takeLatest(ReduxActionTypes.SET_PAGE_ORDER_INIT, setPageOrderSaga),
takeLatest(ReduxActionTypes.POPULATE_PAGEDSLS_INIT, populatePageDSLsSaga),
takeEvery(ReduxActionTypes.UPDATE_CUSTOM_SLUG_INIT, setCustomSlugSaga),
]);
}