PromucFlow_constructor/app/client/src/sagas/TemplatesSagas.ts
Jacques Ikot be308fab43
feat: update template screen UI and building blocks for new users in start from use case flow (#29020)
> Pull Request Template
>
> Use this template to quickly create a well written pull request.
Delete all quotes before creating the pull request.
>
## Description
Updating the UI for the start from use case flow for new users + adding
building blocks to the list of templates offered to new users in this
flow.
>
> Please include a summary of the changes and which issue has been
fixed. Please also include relevant motivation
> and context. List any dependencies that are required for this change
>
> Links to Notion, Figma or any other documents that might be relevant
to the PR
>
https://www.figma.com/file/kbU9xPv44neCfv1FFo9Ndu/User-Activation?node-id=4293:41450&mode=dev
>
https://www.notion.so/appsmith/Activation-experiment-2-Start-from-a-template-Offer-building-blocks-in-addition-to-templates-8986dfc3629041559c81b1650b3a5fe5?pvs=4
#### PR fixes following issue(s)
Fixes #28605 
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [x] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-11-25 07:59:35 +05:30

592 lines
16 KiB
TypeScript

import type {
ApplicationPayload,
ReduxAction,
} from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import {
all,
put,
takeEvery,
call,
select,
take,
fork,
race,
delay,
} from "redux-saga/effects";
import type {
ImportTemplateResponse,
FetchTemplateResponse,
TemplateFiltersResponse,
} from "api/TemplatesApi";
import TemplatesAPI from "api/TemplatesApi";
import history from "utils/history";
import { getDefaultPageId } from "@appsmith/sagas/ApplicationSagas";
import { getDefaultPageId as selectDefaultPageId } from "sagas/selectors";
import {
getAllTemplates,
setTemplateNotificationSeenAction,
showStarterBuildingBlockDatasourcePrompt,
showTemplatesModal,
} from "actions/templateActions";
import {
getTemplateNotificationSeen,
setTemplateNotificationSeen,
} from "utils/storage";
import { validateResponse } from "./ErrorSagas";
import { builderURL } from "@appsmith/RouteBuilder";
import {
getCurrentApplicationId,
getCurrentPageId,
getCurrentPageName,
} from "selectors/editorSelectors";
import { getCurrentWorkspaceId } from "@appsmith/selectors/workspaceSelectors";
import {
fetchApplication,
showReconnectDatasourceModal,
} from "@appsmith/actions/applicationActions";
import { APP_MODE } from "entities/App";
import {
executePageLoadActions,
fetchActions,
} from "actions/pluginActionActions";
import { fetchJSCollections } from "actions/jsActionActions";
import { failFastApiCalls } from "./InitSagas";
import { fetchDatasources } from "actions/datasourceActions";
import { fetchPluginFormConfigs } from "actions/pluginActions";
import { fetchAllPageEntityCompletion, saveLayout } from "actions/pageActions";
import { getAllPageIds } from "./selectors";
import { fetchPageDSLSaga } from "@appsmith/sagas/PageSagas";
import { toast } from "design-system";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { STARTER_BUILDING_BLOCKS } from "constants/TemplatesConstants";
import urlBuilder from "@appsmith/entities/URLRedirect/URLAssembly";
import { fetchJSLibraries } from "actions/JSLibraryActions";
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* forkStarterBuildingBlockToApplicationSaga(
action: ReduxAction<{
pageNames?: string[];
templateId: string;
templateName: string;
}>,
) {
try {
// Get page name and id where the starter template was clicked
const activePageName: string = yield select(getCurrentPageName);
const activePageId: string = yield select(getCurrentPageId);
// Get current default page id
const defaultPageId: string = yield select(selectDefaultPageId);
const {
applicationId,
isValid,
templatePageIds,
}: {
applicationId: string;
isValid: boolean;
prevPageIds: string[];
templatePageIds: string[];
} = yield call(apiCallForForkTemplateToApplicaion, action);
function* deleteExistingEmptyPageInApp(pageId: string) {
yield put({
type: ReduxActionTypes.DELETE_PAGE_INIT,
payload: {
id: pageId,
},
});
}
function* renameStarterTemplatePageToDefault(pageId: string) {
yield put({
type: ReduxActionTypes.UPDATE_PAGE_INIT,
payload: {
id: pageId,
name: activePageName,
isHidden: false,
},
});
}
if (isValid) {
// If the page where the starter template was clicked is the default page
if (activePageId === defaultPageId) {
// 1. Set the template page as home page
yield put({
type: ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_INIT,
payload: {
id: templatePageIds[0],
applicationId,
},
});
yield race([
take(ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_SUCCESS),
take(ReduxActionErrorTypes.SET_DEFAULT_APPLICATION_PAGE_ERROR),
]);
}
// 2. Delete the existing page
yield fork(deleteExistingEmptyPageInApp, activePageId);
// 3. Rename the template page to clicked from page
yield fork(renameStarterTemplatePageToDefault, templatePageIds[0]);
// 4. Wait for page update and delete to complete
yield race([
take(ReduxActionTypes.UPDATE_PAGE_SUCCESS),
take(ReduxActionErrorTypes.UPDATE_PAGE_ERROR),
]);
// 5. Complete the page addition flow
yield put({
type: ReduxActionTypes.IMPORT_STARTER_TEMPLATE_TO_APPLICATION_SUCCESS,
});
// Show datasource prompt after 3 seconds
yield delay(STARTER_BUILDING_BLOCKS.DATASOURCE_PROMPT_DELAY);
yield put(showStarterBuildingBlockDatasourcePrompt(templatePageIds[0]));
} else {
yield put({
type: ReduxActionErrorTypes.IMPORT_STARTER_BUILDING_BLOCK_TO_APPLICATION_ERROR,
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.IMPORT_STARTER_BUILDING_BLOCK_TO_APPLICATION_ERROR,
});
}
}
function* forkTemplateToApplicationSaga(
action: ReduxAction<{
pageNames?: string[];
templateId: string;
templateName: string;
}>,
) {
try {
const {
isValid,
}: {
isValid: boolean;
} = yield call(apiCallForForkTemplateToApplicaion, action);
if (isValid) {
yield put(showTemplatesModal(false));
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_STARTER_BUILDING_BLOCK_TO_APPLICATION_INIT,
forkStarterBuildingBlockToApplicationSaga,
),
takeEvery(
ReduxActionTypes.IMPORT_TEMPLATE_TO_APPLICATION_ONBOARDING_FLOW,
forkTemplateToApplicationViaOnboardingFlowSaga,
),
]);
}