PromucFlow_constructor/app/client/src/sagas/TemplatesSagas.ts
Hetu Nandu 3d1192aba1
chore: Add separate flow for Anvil app publish (#39891)
## Description

Anvil apps can have additional tasks before publishing so separate it
out

Fixes
https://www.notion.so/appsmith/Prompt-users-to-save-tools-on-deploy-1bcfe271b0e2804abe30fe462061f454?pvs=4

## Automation

/ok-to-test tags="@tag.Sanity"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/14052281640>
> Commit: df611bee4f3c54107f2478f97b9263491b2b2e2e
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14052281640&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Tue, 25 Mar 2025 06:04:32 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced an enhanced publishing flow for Anvil applications with
dedicated actions for initiation, success, and error scenarios.
- Updated the deployment behavior to automatically use the Anvil
publishing process when enabled.

- **Refactor**
- Streamlined the code by removing legacy functionality related to
schema generation.
- Consolidated utility methods for determining default pages for
improved consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-03-25 15:25:48 +05:30

498 lines
14 KiB
TypeScript

import { builderURL } from "ee/RouteBuilder";
import {
fetchApplication,
showReconnectDatasourceModal,
} from "ee/actions/applicationActions";
import type { ApplicationPayload } from "entities/Application";
import type { ReduxAction } from "actions/ReduxActionTypes";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import urlBuilder from "ee/entities/URLRedirect/URLAssembly";
import { findDefaultPage } from "pages/utils";
import { fetchPageDSLSaga } from "ee/sagas/PageSagas";
import { getCurrentWorkspaceId } from "ee/selectors/selectedWorkspaceSelectors";
import { isAirgapped } from "ee/utils/airgapHelpers";
import { fetchJSLibraries } from "actions/JSLibraryActions";
import { fetchDatasources } from "actions/datasourceActions";
import { fetchJSCollections } from "actions/jsActionActions";
import { fetchAllPageEntityCompletion, saveLayout } from "actions/pageActions";
import {
executePageLoadActions,
fetchActions,
} from "actions/pluginActionActions";
import { fetchPluginFormConfigs } from "actions/pluginActions";
import {
getAllTemplates,
hideTemplatesModal,
setTemplateNotificationSeenAction,
} from "actions/templateActions";
import type {
FetchTemplateResponse,
ImportTemplateResponse,
TemplateFiltersResponse,
} from "api/TemplatesApi";
import TemplatesAPI from "api/TemplatesApi";
import { toast } from "@appsmith/ads";
import { APP_MODE } from "entities/App";
import { all, call, put, select, take, takeEvery } from "redux-saga/effects";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import history from "utils/history";
import {
getTemplateNotificationSeen,
setTemplateNotificationSeen,
} from "utils/storage";
import { validateResponse } from "./ErrorSagas";
import { failFastApiCalls } from "./InitSagas";
import { getAllPageIdentities } from "./selectors";
const isAirgappedInstance = isAirgapped();
function* getAllTemplatesSaga() {
try {
const response: FetchTemplateResponse = yield call(
TemplatesAPI.getAllTemplates,
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
yield put({
type: ReduxActionTypes.GET_ALL_TEMPLATES_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.GET_ALL_TEMPLATES_ERROR,
payload: {
error,
},
});
}
}
function* importTemplateToWorkspaceSaga(
action: ReduxAction<{ templateId: string; workspaceId: string }>,
) {
try {
const response: ImportTemplateResponse = yield call(
TemplatesAPI.importTemplate,
action.payload.templateId,
action.payload.workspaceId,
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
const defaultPage = findDefaultPage(response.data.application.pages);
const application: ApplicationPayload = {
...response.data.application,
defaultPageId: defaultPage?.id,
defaultBasePageId: defaultPage?.baseId,
};
yield put({
type: ReduxActionTypes.IMPORT_TEMPLATE_TO_WORKSPACE_SUCCESS,
payload: response.data.application,
});
if (response.data.isPartialImport) {
yield put(
showReconnectDatasourceModal({
application: response.data.application,
unConfiguredDatasourceList:
response.data.unConfiguredDatasourceList,
workspaceId: action.payload.workspaceId,
}),
);
} else {
const pageURL = builderURL({
basePageId: application.defaultBasePageId,
});
history.push(pageURL);
}
yield put(getAllTemplates());
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.IMPORT_TEMPLATE_TO_WORKSPACE_ERROR,
payload: {
error,
},
});
}
}
function* getSimilarTemplatesSaga(action: ReduxAction<string>) {
try {
const response: FetchTemplateResponse = yield call(
TemplatesAPI.getSimilarTemplates,
action.payload,
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
yield put({
type: ReduxActionTypes.GET_SIMILAR_TEMPLATES_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.GET_SIMILAR_TEMPLATES_ERROR,
payload: {
error,
},
});
}
}
function* setTemplateNotificationSeenSaga(action: ReduxAction<boolean>) {
yield setTemplateNotificationSeen(action.payload);
}
function* getTemplateNotificationSeenSaga() {
const showTemplateNotification: unknown = yield getTemplateNotificationSeen();
if (showTemplateNotification) {
yield put(setTemplateNotificationSeenAction(true));
} else {
yield put(setTemplateNotificationSeenAction(false));
}
}
function* getTemplateSaga(action: ReduxAction<string>) {
try {
const response: FetchTemplateResponse = yield call(
TemplatesAPI.getTemplateInformation,
action.payload,
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
yield put({
type: ReduxActionTypes.GET_TEMPLATE_SUCCESS,
payload: response.data,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.GET_TEMPLATE_ERROR,
payload: {
error,
},
});
}
}
export function* postPageAdditionSaga(applicationId: string) {
const afterActionsFetch: boolean = yield failFastApiCalls(
[
fetchActions({ applicationId }, []),
fetchJSCollections({ applicationId }),
fetchDatasources(),
fetchJSLibraries(applicationId),
],
[
ReduxActionTypes.FETCH_ACTIONS_SUCCESS,
ReduxActionTypes.FETCH_JS_ACTIONS_SUCCESS,
ReduxActionTypes.FETCH_DATASOURCES_SUCCESS,
ReduxActionTypes.FETCH_JS_LIBRARIES_SUCCESS,
],
[
ReduxActionErrorTypes.FETCH_ACTIONS_ERROR,
ReduxActionErrorTypes.FETCH_JS_ACTIONS_ERROR,
ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR,
ReduxActionErrorTypes.FETCH_JS_LIBRARIES_FAILED,
],
);
if (!afterActionsFetch) {
throw new Error("Failed importing template");
}
const afterPluginFormsFetch: boolean = yield failFastApiCalls(
[fetchPluginFormConfigs()],
[ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS],
[ReduxActionErrorTypes.FETCH_PLUGIN_FORM_CONFIGS_ERROR],
);
if (!afterPluginFormsFetch) {
throw new Error("Failed importing template");
}
yield put(fetchAllPageEntityCompletion([executePageLoadActions()]));
}
function* forkTemplateToApplicationSaga(
action: ReduxAction<{
pageNames?: string[];
templateId: string;
templateName: string;
}>,
) {
try {
const {
isValid,
}: {
isValid: boolean;
} = yield call(apiCallForForkTemplateToApplicaion, action);
if (isValid) {
yield put(hideTemplatesModal());
yield put(getAllTemplates());
toast.show(
`Pages from '${action.payload.templateName}' template added successfully`,
{
kind: "success",
},
);
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.IMPORT_TEMPLATE_TO_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
function* apiCallForForkTemplateToApplicaion(
action: ReduxAction<{
templateId: string;
templateName: string;
pageNames?: string[] | undefined;
}>,
) {
const pagesToImport = action.payload.pageNames
? action.payload.pageNames
: undefined;
const applicationId: string = yield select(getCurrentApplicationId);
const workspaceId: string = yield select(getCurrentWorkspaceId);
const prevPages: { pageId: string; basePageId: string }[] =
yield select(getAllPageIdentities);
const prevPageIds = prevPages.map((page) => page.pageId);
const response: ImportTemplateResponse = yield call(
TemplatesAPI.importTemplateToApplication,
action.payload.templateId,
applicationId,
workspaceId,
pagesToImport,
);
// To fetch the new set of pages after merging the template into the existing application
yield put(
fetchApplication({
mode: APP_MODE.EDIT,
applicationId,
}),
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
yield call(postPageAdditionSaga, applicationId);
const pages: { pageId: string; basePageId: string }[] =
yield select(getAllPageIdentities);
const templatePageIds: string[] = pages
.filter((page) => !prevPageIds.includes(page.pageId))
.map((page) => page.pageId);
const pageDSLs: unknown = yield all(
templatePageIds.map((pageId: string) => {
return call(fetchPageDSLSaga, pageId);
}),
);
yield put({
type: ReduxActionTypes.FETCH_PAGE_DSLS_SUCCESS,
payload: pageDSLs,
});
yield put({
type: ReduxActionTypes.UPDATE_PAGE_LIST,
payload: pageDSLs,
});
if (response.data.isPartialImport) {
yield put(
showReconnectDatasourceModal({
application: response.data.application,
unConfiguredDatasourceList: response.data.unConfiguredDatasourceList,
workspaceId,
pageId: pages[0].pageId,
}),
);
}
history.push(
builderURL({
basePageId: pages[0].basePageId,
}),
);
yield take(ReduxActionTypes.UPDATE_CANVAS_STRUCTURE);
yield put(saveLayout());
yield put({
type: ReduxActionTypes.IMPORT_TEMPLATE_TO_APPLICATION_SUCCESS,
payload: response.data.application,
});
return { isValid, applicationId, templatePageIds, prevPageIds };
}
return { isValid };
}
function* getTemplateFiltersSaga() {
try {
const response: TemplateFiltersResponse = yield call(
TemplatesAPI.getTemplateFilters,
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
yield put({
type: ReduxActionTypes.GET_TEMPLATE_FILTERS_SUCCESS,
payload: response.data,
});
}
} catch (e) {
yield put({
type: ReduxActionErrorTypes.GET_TEMPLATE_FILTERS_ERROR,
payload: {
e,
},
});
}
}
function* forkTemplateToApplicationViaOnboardingFlowSaga(
action: ReduxAction<{
pageNames?: string[];
templateId: string;
templateName: string;
applicationId: string;
workspaceId: string;
}>,
) {
try {
const response: ImportTemplateResponse = yield call(
TemplatesAPI.importTemplateToApplication,
action.payload.templateId,
action.payload.applicationId,
action.payload.workspaceId,
action.payload.pageNames,
);
const isValid: boolean = yield validateResponse(response);
if (isValid) {
const application = response.data.application;
urlBuilder.updateURLParams(
{
applicationSlug: application.slug,
applicationVersion: application.applicationVersion,
baseApplicationId: application.baseId,
},
application.pages.map((page) => ({
pageSlug: page.slug,
customSlug: page.customSlug,
basePageId: page.baseId,
})),
);
history.push(
builderURL({
basePageId: application.pages[0].id,
}),
);
// This is to remove the existing default Page 1 in the new application after template has been imported.
// 1. Set new page as default
const importedTemplatePages = application.pages.filter(
(page) => !page.isDefault,
);
yield put({
type: ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_INIT,
payload: {
id: importedTemplatePages[0].id,
applicationId: application.id,
},
});
yield take(ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_SUCCESS);
const defaultPageId = application.pages.filter(
(page) => page.isDefault,
)[0].id;
//2. Delete old default page (Page 1)
yield put({
type: ReduxActionTypes.DELETE_PAGE_INIT,
payload: {
id: defaultPageId,
},
});
yield put({
type: ReduxActionTypes.IMPORT_TEMPLATE_TO_APPLICATION_ONBOARDING_FLOW_SUCCESS,
payload: response.data.application,
});
toast.show(
`Pages from '${action.payload.templateName}' template added successfully`,
{
kind: "success",
},
);
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.IMPORT_TEMPLATE_TO_APPLICATION_ONBOARDING_FLOW_ERROR,
payload: {
error,
},
});
}
}
// TODO: Refactor and handle this airgap check in a better way - posssibly in root sagas (sangeeth)
export default function* watchActionSagas() {
if (!isAirgappedInstance)
yield all([
takeEvery(ReduxActionTypes.GET_ALL_TEMPLATES_INIT, getAllTemplatesSaga),
takeEvery(ReduxActionTypes.GET_TEMPLATE_INIT, getTemplateSaga),
takeEvery(
ReduxActionTypes.GET_SIMILAR_TEMPLATES_INIT,
getSimilarTemplatesSaga,
),
takeEvery(
ReduxActionTypes.IMPORT_TEMPLATE_TO_WORKSPACE_INIT,
importTemplateToWorkspaceSaga,
),
takeEvery(
ReduxActionTypes.GET_TEMPLATE_NOTIFICATION_SEEN,
getTemplateNotificationSeenSaga,
),
takeEvery(
ReduxActionTypes.SET_TEMPLATE_NOTIFICATION_SEEN,
setTemplateNotificationSeenSaga,
),
takeEvery(
ReduxActionTypes.IMPORT_TEMPLATE_TO_APPLICATION_INIT,
forkTemplateToApplicationSaga,
),
takeEvery(
ReduxActionTypes.GET_TEMPLATE_FILTERS_INIT,
getTemplateFiltersSaga,
),
takeEvery(
ReduxActionTypes.IMPORT_TEMPLATE_TO_APPLICATION_ONBOARDING_FLOW,
forkTemplateToApplicationViaOnboardingFlowSaga,
),
]);
}