## 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 -->
498 lines
14 KiB
TypeScript
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,
|
|
),
|
|
]);
|
|
}
|