PromucFlow_constructor/app/client/src/ce/sagas/ApplicationSagas.tsx
Pawan Kumar d036064beb
chore: Update flags for self-hosting agents (#40639)
/ok-to-test tags="@tag.Sanity"

<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/14994848262>
> Commit: 64bdb8cd0606bbc4c1b11d69b2d0e7cd7b5dd78a
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=14994848262&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Sanity`
> Spec:
> <hr>Tue, 13 May 2025 11:39:41 UTC
<!-- 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 a new feature flag for AI agent instances, allowing for
more granular control over AI agent-related functionality.

- **Refactor**
- Updated various components and selectors to use the new AI agent
instance feature flag and related selectors, replacing previous flags
and logic.
- Separated agent and non-agent template handling for clearer template
management.
- Improved feature flag override capabilities, enabling dynamic
overrides via external sources.
- Added new selectors to better represent AI agent app and creation
states.
- Refined UI components and modals to reflect updated AI agent state
flags.
- Enhanced user authentication and signup flows with updated feature
flag conditions.

- **Bug Fixes**
- Ensured consistent UI behavior and conditional rendering based on the
updated feature flag logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-05-13 17:36:58 +05:30

1175 lines
35 KiB
TypeScript

import type { ReduxAction } from "actions/ReduxActionTypes";
import {
ReduxActionErrorTypes,
ReduxActionTypes,
} from "ee/constants/ReduxActionConstants";
import type {
ApplicationResponsePayload,
ChangeAppViewAccessRequest,
CreateApplicationRequest,
CreateApplicationResponse,
DeleteApplicationRequest,
DeleteNavigationLogoRequest,
FetchApplicationPayload,
FetchApplicationResponse,
FetchApplicationsOfWorkspaceResponse,
FetchUnconfiguredDatasourceListResponse,
FetchUsersApplicationsWorkspacesResponse,
ForkApplicationRequest,
ImportApplicationRequest,
PublishApplicationRequest,
PublishApplicationResponse,
SetDefaultPageRequest,
UpdateApplicationRequest,
UpdateApplicationResponse,
UploadNavigationLogoRequest,
} from "ee/api/ApplicationApi";
import ApplicationApi from "ee/api/ApplicationApi";
import { all, call, fork, put, select, take } from "redux-saga/effects";
import { validateResponse } from "sagas/ErrorSagas";
import {
getCurrentApplication,
getCurrentApplicationIdForCreateNewApp,
} from "ee/selectors/applicationSelectors";
import type { ApiResponse } from "api/ApiResponses";
import history from "utils/history";
import type { DefaultRootState } from "react-redux";
import {
ApplicationVersion,
deleteApplicationNavigationLogoSuccessAction,
fetchApplication,
importApplicationSuccess,
initDatasourceConnectionDuringImportSuccess,
resetCurrentApplication,
setDefaultApplicationPageSuccess,
setIsReconnectingDatasourcesModalOpen,
setPageIdForImport,
setWorkspaceIdForImport,
showReconnectDatasourceModal,
updateApplicationNavigationLogoSuccessAction,
updateApplicationNavigationSettingAction,
updateCurrentApplicationEmbedSetting,
updateCurrentApplicationIcon,
updateCurrentApplicationForkingEnabled,
updateApplicationThemeSettingAction,
fetchAllApplicationsOfWorkspace,
publishApplication,
} from "ee/actions/applicationActions";
import AnalyticsUtil from "ee/utils/AnalyticsUtil";
import {
createMessage,
ERROR_IMPORTING_APPLICATION_TO_WORKSPACE,
IMPORT_APP_SUCCESSFUL,
} from "ee/constants/messages";
import { APP_MODE } from "entities/App";
import type { Workspace } from "ee/constants/workspaceConstants";
import type { AppColorCode } from "constants/DefaultTheme";
import {
getCurrentApplicationId,
getCurrentBasePageId,
getCurrentPageId,
getIsEditorInitialized,
} from "selectors/editorSelectors";
import {
deleteRecentAppEntities,
getEnableStartSignposting,
} from "utils/storage";
import { getFetchedWorkspaces } from "ee/selectors/workspaceSelectors";
import { fetchPluginFormConfigs, fetchPlugins } from "actions/pluginActions";
import {
fetchDatasources,
setUnconfiguredDatasourcesDuringImport,
} from "actions/datasourceActions";
import { failFastApiCalls } from "sagas/InitSagas";
import type { Datasource } from "entities/Datasource";
import { builderURL, viewerURL } from "ee/RouteBuilder";
import { getDefaultPageId as selectDefaultPageId } from "sagas/selectors";
import PageApi from "api/PageApi";
import { isEmpty, merge } from "lodash";
import { checkAndGetPluginFormConfigsSaga } from "sagas/PluginSagas";
import { getPageList, getPluginForm } from "ee/selectors/entitiesSelector";
import { getConfigInitialValues } from "components/formControls/utils";
import DatasourcesApi from "ee/api/DatasourcesApi";
import type { SetDefaultPageActionPayload } from "actions/pageActions";
import { resetApplicationWidgets } from "actions/pageActions";
import { setCanvasCardsState } from "actions/editorActions";
import { toast } from "@appsmith/ads";
import type { User } from "constants/userConstants";
import { ANONYMOUS_USERNAME } from "constants/userConstants";
import { getCurrentUser } from "selectors/usersSelectors";
import { ERROR_CODES } from "ee/constants/ApiConstants";
import { safeCrashAppRequest } from "actions/errorActions";
import type { IconNames } from "@appsmith/ads";
import {
defaultNavigationSetting,
keysOfNavigationSetting,
type NavigationSetting,
} from "constants/AppConstants";
import { setAllEntityCollapsibleStates } from "actions/editorContextActions";
import { getCurrentEnvironmentId } from "ee/selectors/environmentSelectors";
import { LayoutSystemTypes } from "layoutSystems/types";
import {
getApplicationsOfWorkspace,
getCurrentWorkspaceId,
} from "ee/selectors/selectedWorkspaceSelectors";
import equal from "fast-deep-equal";
import { getFromServerWhenNoPrefetchedResult } from "sagas/helper";
import type { Page } from "entities/Page";
import type { ApplicationPayload } from "entities/Application";
import { objectKeys } from "@appsmith/utils";
import { findDefaultPage } from "pages/utils";
import { getIsAiAgentInstanceEnabled } from "ee/selectors/aiAgentSelectors";
export let windowReference: Window | null = null;
const AI_DATASOURCE_NAME = "AI Datasource";
export function* publishApplicationSaga(
requestAction: ReduxAction<PublishApplicationRequest>,
) {
const currentApplication: ApplicationPayload | undefined = yield select(
getCurrentApplication,
);
if (currentApplication) {
const appName = currentApplication.name;
const appId = currentApplication?.id;
const pageCount = currentApplication?.pages?.length;
const navigationSettingsWithPrefix: Record<
string,
NavigationSetting[keyof NavigationSetting]
> = {};
if (currentApplication.applicationDetail?.navigationSetting) {
const settingKeys = objectKeys(
currentApplication.applicationDetail.navigationSetting,
) as Array<keyof NavigationSetting>;
settingKeys.map((key: keyof NavigationSetting) => {
if (currentApplication.applicationDetail?.navigationSetting?.[key]) {
const value: NavigationSetting[keyof NavigationSetting] =
currentApplication.applicationDetail.navigationSetting[key];
navigationSettingsWithPrefix[`navigationSetting_${key}`] = value;
}
});
}
AnalyticsUtil.logEvent("PUBLISH_APP", {
appId,
appName,
pageCount,
...navigationSettingsWithPrefix,
isPublic: !!currentApplication?.isPublic,
templateTitle: currentApplication?.forkedFromTemplateTitle,
});
}
try {
const request = requestAction.payload;
const response: PublishApplicationResponse = yield call(
ApplicationApi.publishApplication,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.PUBLISH_APPLICATION_SUCCESS,
});
const applicationId: string = yield select(getCurrentApplicationId);
const currentBasePageId: string = yield select(getCurrentBasePageId);
const currentPageId: string = yield select(getCurrentPageId);
const appicationViewPageUrl = viewerURL({
basePageId: currentBasePageId,
});
yield put(
fetchApplication({
applicationId,
pageId: currentPageId,
mode: APP_MODE.EDIT,
}),
);
// If the tab is opened focus and reload else open in new tab
if (!windowReference || windowReference.closed) {
windowReference = window.open(appicationViewPageUrl, "_blank");
} else {
windowReference.focus();
windowReference.location.href =
windowReference.location.origin + appicationViewPageUrl;
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.PUBLISH_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
export function* fetchAllApplicationsOfWorkspaceSaga(
action?: ReduxAction<string>,
) {
let activeWorkspaceId: string = "";
if (!action?.payload) {
activeWorkspaceId = yield select(getCurrentWorkspaceId);
} else {
activeWorkspaceId = action.payload;
}
try {
const response: FetchApplicationsOfWorkspaceResponse = yield call(
ApplicationApi.fetchAllApplicationsOfWorkspace,
activeWorkspaceId,
);
const workspaces: Workspace[] = yield select(getFetchedWorkspaces);
const isOnboardingApplicationId: string = yield select(
getCurrentApplicationIdForCreateNewApp,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
const applications = response.data.map((application) => {
const defaultPage = findDefaultPage(application.pages);
return {
...application,
defaultPageId: defaultPage?.id,
defaultBasePageId: defaultPage?.baseId,
};
});
yield put({
type: ReduxActionTypes.FETCH_ALL_APPLICATIONS_OF_WORKSPACE_SUCCESS,
payload: applications,
});
// This will initialise the current workspace to first only during onboarding
if (workspaces.length > 0 && !!isOnboardingApplicationId) {
yield put({
type: ReduxActionTypes.SET_CURRENT_WORKSPACE,
payload: workspaces[0],
});
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FETCH_USER_APPLICATIONS_WORKSPACES_ERROR,
payload: {
error,
},
});
}
}
// v1
export function* fetchAppAndPagesSaga(
action: ReduxAction<FetchApplicationPayload>,
) {
try {
const { pages, ...payload } = action.payload;
const request = { ...payload };
if (request.pageId && request.applicationId) {
delete request.applicationId;
}
const response: FetchApplicationResponse = yield call(
getFromServerWhenNoPrefetchedResult,
pages,
() => call(PageApi.fetchAppAndPages, request),
);
const isValidResponse: boolean = yield call(validateResponse, response);
if (isValidResponse) {
const prevPagesState: Page[] = yield select(getPageList);
const pagePermissionsMap = prevPagesState.reduce(
(acc, page) => {
acc[page.pageId] = page.userPermissions ?? [];
return acc;
},
{} as Record<string, string[]>,
);
yield put({
type: ReduxActionTypes.FETCH_APPLICATION_SUCCESS,
payload: { ...response.data.application, pages: response.data.pages },
});
yield put({
type: ReduxActionTypes.FETCH_PAGE_LIST_SUCCESS,
payload: {
pages: response.data.pages.map((page) => ({
pageName: page.name,
pageId: page.id,
basePageId: page.baseId,
isDefault: page.isDefault,
isHidden: !!page.isHidden,
slug: page.slug,
customSlug: page.customSlug,
userPermissions: page.userPermissions
? page.userPermissions
: pagePermissionsMap[page.id],
})),
applicationId: response.data.application?.id,
baseApplicationId: response.data.application?.baseId,
},
});
yield put({
type: ReduxActionTypes.SET_CURRENT_WORKSPACE_ID,
payload: {
workspaceId: response.data.workspaceId,
editorId: response.data.application?.id,
mode: request.mode,
},
});
yield put({
type: ReduxActionTypes.SET_APP_VERSION_ON_WORKER,
payload: response.data.application?.evaluationVersion,
});
} else {
yield call(handleFetchApplicationError, response.responseMeta?.error);
}
} catch (error) {
yield call(handleFetchApplicationError, error);
}
}
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function* handleFetchApplicationError(error: any) {
const currentUser: User = yield select(getCurrentUser);
if (
currentUser &&
currentUser.email === ANONYMOUS_USERNAME &&
error?.code === ERROR_CODES.PAGE_NOT_FOUND
) {
yield put(safeCrashAppRequest(ERROR_CODES.PAGE_NOT_FOUND));
} else {
yield put({
type: ReduxActionErrorTypes.FETCH_APPLICATION_ERROR,
payload: {
error,
},
});
yield put({
type: ReduxActionErrorTypes.FETCH_PAGE_LIST_ERROR,
payload: {
error,
},
});
}
}
export function* setDefaultApplicationPageSaga(
action: ReduxAction<SetDefaultPageActionPayload>,
) {
try {
const defaultPageId: string = yield select(selectDefaultPageId);
if (defaultPageId !== action.payload.id) {
const request: SetDefaultPageRequest = {
...action.payload,
pageId: action.payload.id,
};
const response: ApiResponse = yield call(
ApplicationApi.setDefaultApplicationPage,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put(
setDefaultApplicationPageSuccess(
request.pageId,
request.applicationId,
),
);
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.SET_DEFAULT_APPLICATION_PAGE_ERROR,
payload: {
error,
},
});
}
}
export function* updateApplicationLayoutSaga(
action: ReduxAction<UpdateApplicationRequest>,
) {
try {
yield call(updateApplicationSaga, action);
yield put({
type: ReduxActionTypes.CURRENT_APPLICATION_LAYOUT_UPDATE,
payload: action.payload.appLayout,
});
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPDATE_APP_LAYOUT_ERROR,
payload: {
error,
},
});
}
}
export function* updateApplicationSaga(
action: ReduxAction<UpdateApplicationRequest>,
) {
try {
const request: UpdateApplicationRequest = action.payload;
const response: ApiResponse<UpdateApplicationResponse> = yield call(
ApplicationApi.updateApplication,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
// as the redux store updates the app only on success.
// we have to run this
if (isValidResponse) {
if (request && request.applicationVersion) {
if (request.applicationVersion === ApplicationVersion.SLUG_URL) {
request.callback?.();
return;
}
}
if (request) {
yield put({
type: ReduxActionTypes.UPDATE_APPLICATION_SUCCESS,
payload: response.data,
});
}
if (request.currentApp) {
if (request.name)
yield put({
type: ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE,
payload: response.data,
});
if (request.icon) {
yield put(updateCurrentApplicationIcon(response.data.icon));
}
if (request.embedSetting) {
yield put(
updateCurrentApplicationEmbedSetting(response.data.embedSetting),
);
}
if ("forkingEnabled" in request) {
yield put(
updateCurrentApplicationForkingEnabled(
response.data.forkingEnabled,
),
);
}
if (
request.applicationDetail?.navigationSetting &&
response.data.applicationDetail?.navigationSetting
) {
yield put(
updateApplicationNavigationSettingAction(
response.data.applicationDetail.navigationSetting,
),
);
}
// TODO: refactor this once backend is ready
if (request.applicationDetail?.themeSetting) {
yield put(
updateApplicationThemeSettingAction(
request.applicationDetail?.themeSetting,
),
);
}
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPDATE_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
export function* deleteApplicationSaga(
action: ReduxAction<DeleteApplicationRequest>,
) {
try {
const request: DeleteApplicationRequest = action.payload;
const response: ApiResponse = yield call(
ApplicationApi.deleteApplication,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.DELETE_APPLICATION_SUCCESS,
payload: response.data,
});
yield call(deleteRecentAppEntities, request.applicationId);
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.DELETE_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
export function* changeAppViewAccessSaga(
requestAction: ReduxAction<ChangeAppViewAccessRequest>,
) {
try {
const request = requestAction.payload;
const response: ApiResponse = yield call(
ApplicationApi.changeAppViewAccess,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.CHANGE_APPVIEW_ACCESS_SUCCESS,
payload: {
// @ts-expect-error: response is of type unknown
id: response.data.id,
// @ts-expect-error: response is of type unknown
isPublic: response.data.isPublic,
},
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CHANGE_APPVIEW_ACCESS_ERROR,
payload: {
error,
},
});
}
}
export function* createApplicationSaga(
action: ReduxAction<{
applicationName: string;
icon: IconNames;
color: AppColorCode;
workspaceId: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolve: any;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: any;
}>,
) {
const { applicationName, color, icon, reject, workspaceId } = action.payload;
try {
const applications: Workspace[] = yield select(getApplicationsOfWorkspace);
const existingApplication = applications
? applications.find((application) => application.name === applicationName)
: null;
if (existingApplication) {
yield call(reject, {
_error: "An application with this name already exists",
});
yield put({
type: ReduxActionErrorTypes.CREATE_APPLICATION_ERROR,
payload: {
error: "Could not create application",
show: false,
},
});
} else {
yield put(resetCurrentApplication());
const request: CreateApplicationRequest = {
name: applicationName,
icon: icon,
color: color,
workspaceId,
layoutSystemType: LayoutSystemTypes.FIXED, // Note: This may be provided as an action payload in the future
};
const response: CreateApplicationResponse = yield call(
ApplicationApi.createApplication,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
const defaultPage = findDefaultPage(response.data.pages);
const application: ApplicationPayload = {
...response.data,
defaultPageId: defaultPage?.id,
defaultBasePageId: defaultPage?.baseId,
};
AnalyticsUtil.logEvent("CREATE_APP", {
appName: application.name,
});
// This sets ui.pageWidgets = {} to ensure that
// widgets are cleaned up from state before
// finishing creating a new application
yield put(resetApplicationWidgets());
yield put({
type: ReduxActionTypes.CREATE_APPLICATION_SUCCESS,
payload: {
workspaceId,
application,
},
});
// All new apps will have the Entity Explorer unfurled so that users
// can find the entities they have created
yield put(
setAllEntityCollapsibleStates({
Widgets: true,
["Queries/JS"]: true,
Datasources: true,
}),
);
const enableSignposting: boolean | null =
yield getEnableStartSignposting();
if (enableSignposting) {
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
payload: application.id,
});
}
yield put(setCanvasCardsState(defaultPage?.id ?? ""));
history.push(
builderURL({
basePageId: defaultPage?.baseId,
}),
);
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.CREATE_APPLICATION_ERROR,
payload: {
error,
show: false,
workspaceId,
},
});
}
}
export function* forkApplicationSaga(
action: ReduxAction<ForkApplicationRequest>,
) {
try {
const response: ApiResponse<{
application: ApplicationResponsePayload;
isPartialImport: boolean;
unConfiguredDatasourceList: Datasource[];
}> = yield call(ApplicationApi.forkApplication, {
applicationId: action.payload.applicationId,
workspaceId: action.payload.workspaceId,
});
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put(resetCurrentApplication());
const defaultPage = findDefaultPage(response.data.application.pages);
const application: ApplicationPayload = {
...response.data.application,
defaultPageId: defaultPage?.id,
defaultBasePageId: defaultPage?.baseId,
};
yield put({
type: ReduxActionTypes.FORK_APPLICATION_SUCCESS,
payload: {
workspaceId: action.payload.workspaceId,
application,
},
});
yield put({
type: ReduxActionTypes.SET_CURRENT_WORKSPACE_ID,
payload: {
workspaceId: action.payload.workspaceId,
editorId: application.id,
},
});
const pageURL = builderURL({
basePageId: defaultPage?.baseId,
params: { branch: null },
});
if (action.payload.editMode) {
yield put({
type: ReduxActionTypes.FETCH_APPLICATION_INIT,
payload: {
applicationId: application.id,
pageId: defaultPage?.id,
},
});
}
history.push(pageURL);
const isEditorInitialized: boolean = yield select(getIsEditorInitialized);
if (!isEditorInitialized) {
yield take(ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS);
}
// Temporary fix to remove AI Datasource from the unConfiguredDatasourceList
// so we can avoid showing the AI Datasource in reconnect datasource modal
const filteredUnConfiguredDatasourceList = (
response?.data?.unConfiguredDatasourceList || []
).filter(
(datasource) => datasource.name !== AI_DATASOURCE_NAME,
) as Datasource[];
if (
response.data.isPartialImport &&
filteredUnConfiguredDatasourceList.length > 0
) {
yield put(
showReconnectDatasourceModal({
application: response.data?.application,
unConfiguredDatasourceList: filteredUnConfiguredDatasourceList,
workspaceId: action.payload.workspaceId,
}),
);
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FORK_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
export function* showReconnectDatasourcesModalSaga(
action: ReduxAction<{
application: ApplicationResponsePayload;
unConfiguredDatasourceList: Array<Datasource>;
workspaceId: string;
pageId?: string;
}>,
) {
const { application, pageId, unConfiguredDatasourceList, workspaceId } =
action.payload;
yield put(fetchAllApplicationsOfWorkspace());
yield put(importApplicationSuccess(application));
yield put(fetchPlugins({ workspaceId }));
yield put(
setUnconfiguredDatasourcesDuringImport(unConfiguredDatasourceList || []),
);
yield put(setWorkspaceIdForImport({ editorId: application.id, workspaceId }));
yield put(setPageIdForImport(pageId));
yield put(setIsReconnectingDatasourcesModalOpen({ isOpen: true }));
}
export function* importApplicationSaga(
action: ReduxAction<ImportApplicationRequest>,
) {
try {
const response: ApiResponse<{
unConfiguredDatasourceList: Datasource[];
application: ApplicationResponsePayload;
isPartialImport: boolean;
}> = yield call(
ApplicationApi.importApplicationToWorkspace,
action.payload,
);
const urlObject = new URL(window.location.href);
const isApplicationUrl = urlObject.pathname.includes("/app/");
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
const currentWorkspaceId: string = yield select(getCurrentWorkspaceId);
const allWorkspaces: Workspace[] = yield select(getFetchedWorkspaces);
const currentWorkspace = allWorkspaces.filter(
(el: Workspace) => el.id === action.payload.workspaceId,
);
if (currentWorkspaceId || currentWorkspace.length > 0) {
const {
application: { pages },
isPartialImport,
} = response.data;
yield put(importApplicationSuccess(response.data?.application));
// Temporary fix to remove AI Datasource from the unConfiguredDatasourceList
// so we can avoid showing the AI Datasource in reconnect datasource modal
const filteredUnConfiguredDatasourceList = (
response?.data?.unConfiguredDatasourceList || []
).filter(
(datasource) => datasource.name !== AI_DATASOURCE_NAME,
) as Datasource[];
if (isPartialImport && filteredUnConfiguredDatasourceList.length > 0) {
yield put(
showReconnectDatasourceModal({
application: response.data?.application,
unConfiguredDatasourceList: filteredUnConfiguredDatasourceList,
workspaceId: action.payload.workspaceId,
}),
);
} else {
// TODO: Update route params here
const { application } = response.data;
const defaultPage = findDefaultPage(pages);
const pageURL = builderURL({
basePageId: defaultPage?.baseId,
});
if (isApplicationUrl) {
const appId = application.id;
// @ts-expect-error: defaultPageId does not exist in the application response object
const pageId = application.defaultPageId;
yield put({
type: ReduxActionTypes.FETCH_APPLICATION_INIT,
payload: {
applicationId: appId,
pageId,
},
});
}
history.push(pageURL);
toast.show(createMessage(IMPORT_APP_SUCCESSFUL), {
kind: "success",
});
}
} else {
yield put({
type: ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR,
payload: {
error: createMessage(ERROR_IMPORTING_APPLICATION_TO_WORKSPACE),
},
});
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
export function* fetchReleases() {
try {
const response: FetchUsersApplicationsWorkspacesResponse = yield call(
ApplicationApi.getReleaseItems,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
const { newReleasesCount, releaseItems } = response.data || {};
yield put({
type: ReduxActionTypes.FETCH_RELEASES_SUCCESS,
payload: { newReleasesCount, releaseItems },
});
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.FETCH_RELEASES_ERROR,
payload: {
error,
},
});
}
}
export function* fetchUnconfiguredDatasourceList(
action: ReduxAction<{
applicationId: string;
workspaceId: string;
}>,
) {
try {
// Get endpoint based on app mode
const response: FetchUnconfiguredDatasourceListResponse = yield call(
ApplicationApi.fetchUnconfiguredDatasourceList,
action.payload,
);
yield put(setUnconfiguredDatasourcesDuringImport(response.data || []));
} catch (error) {
yield put(setUnconfiguredDatasourcesDuringImport([]));
yield put({
type: ReduxActionErrorTypes.FETCH_APPLICATION_ERROR,
payload: {
error,
},
});
}
}
export function* initializeDatasourceWithDefaultValues(datasource: Datasource) {
let currentEnvironment: string = yield select(getCurrentEnvironmentId);
if (!datasource.datasourceStorages.hasOwnProperty(currentEnvironment)) {
// if the currentEnvironemnt is not present for use here, take the first key from datasourceStorages
currentEnvironment = Object.keys(datasource.datasourceStorages)[0];
}
const dsStorage = datasource.datasourceStorages[currentEnvironment];
if (!dsStorage?.isConfigured) {
yield call(checkAndGetPluginFormConfigsSaga, datasource.pluginId);
const formConfig: Record<string, unknown>[] = yield select(
getPluginForm,
datasource.pluginId,
);
const initialValues: unknown = yield call(
getConfigInitialValues,
formConfig,
false,
false,
);
const payload = merge(initialValues, dsStorage);
payload.isConfigured = false; // imported datasource as not configured yet
let isDSValueUpdated = false;
if (isEmpty(dsStorage.datasourceConfiguration)) {
isDSValueUpdated = true;
} else {
isDSValueUpdated = !equal(payload, dsStorage);
}
if (isDSValueUpdated) {
const response: ApiResponse =
yield DatasourcesApi.updateDatasourceStorage(payload);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
yield put({
type: ReduxActionTypes.UPDATE_DATASOURCE_IMPORT_SUCCESS,
payload: response.data,
});
}
}
}
}
export function* initDatasourceConnectionDuringImport(
action: ReduxAction<{
workspaceId: string;
isPartialImport?: boolean;
}>,
) {
const workspaceId = action.payload.workspaceId;
const isAgentFlowEnabled: boolean = yield select(getIsAiAgentInstanceEnabled);
const pluginsAndDatasourcesCalls: boolean = yield failFastApiCalls(
[fetchPlugins({ workspaceId }), fetchDatasources({ workspaceId })],
[
ReduxActionTypes.FETCH_PLUGINS_SUCCESS,
ReduxActionTypes.FETCH_DATASOURCES_SUCCESS,
],
[
ReduxActionErrorTypes.FETCH_PLUGINS_ERROR,
ReduxActionErrorTypes.FETCH_DATASOURCES_ERROR,
],
);
if (!pluginsAndDatasourcesCalls) return;
const pluginFormCall: boolean = yield failFastApiCalls(
[fetchPluginFormConfigs()],
[ReduxActionTypes.FETCH_PLUGIN_FORM_CONFIGS_SUCCESS],
[ReduxActionErrorTypes.FETCH_PLUGIN_FORM_CONFIGS_ERROR],
);
if (!pluginFormCall) return;
const datasources: Datasource[] = yield select((state: DefaultRootState) => {
return state.entities.datasources.list;
});
yield all(
datasources.map((datasource: Datasource) => {
if (isAgentFlowEnabled) {
return fork(initializeDatasourceWithDefaultValues, datasource);
}
return call(initializeDatasourceWithDefaultValues, datasource);
}),
);
if (!action.payload.isPartialImport) {
// This is required for reconnect datasource modal popup
yield put(initDatasourceConnectionDuringImportSuccess());
}
}
export function* uploadNavigationLogoSaga(
action: ReduxAction<UploadNavigationLogoRequest>,
) {
try {
const request: UploadNavigationLogoRequest = action.payload;
const response: ApiResponse<UpdateApplicationResponse> = yield call(
ApplicationApi.uploadNavigationLogo,
request,
);
const isValidResponse: boolean = yield validateResponse(response);
if (isValidResponse) {
if (request.logo) {
if (
request.logo &&
response.data.applicationDetail?.navigationSetting?.logoAssetId
) {
yield put(
updateApplicationNavigationLogoSuccessAction(
response.data.applicationDetail.navigationSetting.logoAssetId,
),
);
/**
* When the user creates a new application and they upload logo without
* interacting with any other navigation settings first, we get only
* navigationSetting = { logoAssetId: <id_string_here> } in the API response.
*
* Therefore, we need to handle this case by hitting the update application
* API and store the default navigation settings as well alongside
* the logoAssetId.
*/
const navigationSettingKeys = Object.keys(
response.data.applicationDetail?.navigationSetting,
);
if (
navigationSettingKeys?.length === 1 &&
navigationSettingKeys?.[0] === keysOfNavigationSetting.logoAssetId
) {
const newUpdateApplicationRequestWithDefaultNavigationSettings = {
...response.data,
applicationDetail: {
...response.data.applicationDetail,
navigationSetting: {
...defaultNavigationSetting,
...response.data.applicationDetail.navigationSetting,
},
},
};
const updateApplicationResponse: ApiResponse<UpdateApplicationResponse> =
yield call(
ApplicationApi.updateApplication,
newUpdateApplicationRequestWithDefaultNavigationSettings,
);
if (updateApplicationResponse?.data) {
yield put({
type: ReduxActionTypes.UPDATE_APPLICATION_SUCCESS,
payload: updateApplicationResponse.data,
});
if (
newUpdateApplicationRequestWithDefaultNavigationSettings
.applicationDetail?.navigationSetting &&
updateApplicationResponse.data.applicationDetail
?.navigationSetting
) {
yield put(
updateApplicationNavigationSettingAction(
updateApplicationResponse.data.applicationDetail
.navigationSetting,
),
);
}
}
}
}
}
}
} catch (error) {
yield put({
type: ReduxActionErrorTypes.UPLOAD_NAVIGATION_LOGO_ERROR,
payload: {
error,
},
});
}
}
export function* deleteNavigationLogoSaga(
action: ReduxAction<DeleteNavigationLogoRequest>,
) {
try {
const request: DeleteNavigationLogoRequest = action.payload;
yield call(ApplicationApi.deleteNavigationLogo, request);
yield put(deleteApplicationNavigationLogoSuccessAction());
} catch (error) {
yield put({
type: ReduxActionErrorTypes.DELETE_NAVIGATION_LOGO_ERROR,
payload: {
error,
},
});
}
}
export function* publishAnvilApplicationSaga(
action: ReduxAction<PublishApplicationRequest>,
) {
try {
yield put(publishApplication(action.payload.applicationId));
} catch (error) {
yield put({
type: ReduxActionErrorTypes.PUBLISH_ANVIL_APPLICATION_ERROR,
payload: {
error,
},
});
}
}