## Description <ins>Root cause: </ins> The original intention of the building blocks on canvas was to serve a new user on their very first app. Looking at (good) usage of this feature, we turned it on for all pages on all apps. Now, an experienced user don't always start with UI and thats when we hit this issue. Additionally, this was a tech debt we had to repay and this PR takes that opportunity to get rid of hack of using `add a page from template` for this feature and uses proper PIE based API to support his. Fixes #32573 _or_ Fixes `Issue URL` > [!WARNING] > _If no issue exists, please create an issue first, and check with the maintainers if the issue is valid._ ## Automation /ok-to-test tags="@tag.Templates" ### 🔍 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/8797225643> > Commit: 5f7f0fd094284faf67338412a57ef4757eb70af8 > Cypress dashboard url: <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=8797225643&attempt=1" target="_blank">Click here!</a> <!-- end of auto-generated comment: Cypress test results --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced functionality for importing starter building blocks into applications, including layout positioning and action triggers upon successful import. - Added new actions and selectors in Redux for managing current forking building block names. - **Refactor** - Updated the handling of template names and constants for better consistency and use within the app. - Simplified event handling and variable naming in several components for improved code clarity and performance. - **Bug Fixes** - Adjusted analytics and event data handling to ensure accurate tracking and functionality. - **Tests** - Updated Cypress E2E tests to reflect changes in functionality and removed outdated test cases. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
478 lines
13 KiB
TypeScript
478 lines
13 KiB
TypeScript
import { builderURL } from "@appsmith/RouteBuilder";
|
|
import {
|
|
fetchApplication,
|
|
showReconnectDatasourceModal,
|
|
} from "@appsmith/actions/applicationActions";
|
|
import type {
|
|
ApplicationPayload,
|
|
ReduxAction,
|
|
} from "@appsmith/constants/ReduxActionConstants";
|
|
import {
|
|
ReduxActionErrorTypes,
|
|
ReduxActionTypes,
|
|
} from "@appsmith/constants/ReduxActionConstants";
|
|
import urlBuilder from "@appsmith/entities/URLRedirect/URLAssembly";
|
|
import { getDefaultPageId } from "@appsmith/sagas/ApplicationSagas";
|
|
import { fetchPageDSLSaga } from "@appsmith/sagas/PageSagas";
|
|
import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors";
|
|
import { isAirgapped } from "@appsmith/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 "design-system";
|
|
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 { getAllPageIds } 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 application: ApplicationPayload = {
|
|
...response.data.application,
|
|
defaultPageId: getDefaultPageId(
|
|
response.data.application.pages,
|
|
) as string,
|
|
};
|
|
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({
|
|
pageId: application.defaultPageId,
|
|
});
|
|
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 prevPageIds: string[] = yield select(getAllPageIds);
|
|
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: string[] = yield select(getAllPageIds);
|
|
const templatePageIds: string[] = pages.filter(
|
|
(pageId) => !prevPageIds.includes(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],
|
|
}),
|
|
);
|
|
}
|
|
history.push(
|
|
builderURL({
|
|
pageId: pages[0],
|
|
}),
|
|
);
|
|
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,
|
|
applicationId: application.id,
|
|
},
|
|
application.pages.map((page) => ({
|
|
pageSlug: page.slug,
|
|
customSlug: page.customSlug,
|
|
pageId: page.id,
|
|
})),
|
|
);
|
|
history.push(
|
|
builderURL({
|
|
pageId: 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,
|
|
),
|
|
]);
|
|
}
|