## TL;DR A new revamped experience for navigation for Appsmith users. ## Description Introduces new navigation styles with better default navigation - Top (Stacked), a variant for Top (Inline), and a collapsible Sidebar. Configure your app's navigation by navigating to the navigation settings tab inside the app settings pane and observe how your app with the selected navigation settings will look side by side as you change them. This PR pushes the v1 for EPIC #17766. Fixes #19157 Fixes #19158 Fixes #19174 Fixes #19173 Fixes #19160 Fixes #20712 Fixes #19161 Fixes #20554 Fixes #20938 Fixes #21129 ## Media <video src="https://user-images.githubusercontent.com/22471214/227187245-84e4e3fa-18e4-4690-8237-cfce29f432e5.mp4"></video> ## Type of change - New feature (non-breaking change which adds functionality) - This change requires a documentation update ## How Has This Been Tested? - Manual - Cypress ### Test Plan https://www.notion.so/appsmith/Test-Plan-a7883ae4980d470690de5c62a41dd168 ### Issues raised during DP testing https://docs.google.com/spreadsheets/d/1Kocq8h1H3EXlbqDgiNruzBr9MeNPyY26zct8IWYEY40/edit#gid=0 ## 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 - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] PR is being merged under a feature flag ### QA activity: - [ ] Test plan has been approved by relevant developers - [ ] Test plan has been peer reviewed by QA - [ ] Cypress test cases have been added and approved by either SDET or manual QA - [ ] Organized project review call with relevant stakeholders after Round 1/2 of QA - [ ] Added Test Plan Approved label after reveiwing all Cypress test --------- Co-authored-by: Pawan Kumar <pawan@appsmith.com>
931 lines
28 KiB
TypeScript
931 lines
28 KiB
TypeScript
import type {
|
|
ApplicationPayload,
|
|
Page,
|
|
ReduxAction,
|
|
} from "@appsmith/constants/ReduxActionConstants";
|
|
import {
|
|
ReduxActionErrorTypes,
|
|
ReduxActionTypes,
|
|
} from "@appsmith/constants/ReduxActionConstants";
|
|
import type {
|
|
ApplicationObject,
|
|
ApplicationPagePayload,
|
|
ApplicationResponsePayload,
|
|
ChangeAppViewAccessRequest,
|
|
CreateApplicationRequest,
|
|
CreateApplicationResponse,
|
|
DeleteApplicationRequest,
|
|
DuplicateApplicationRequest,
|
|
FetchApplicationPayload,
|
|
FetchApplicationResponse,
|
|
FetchUnconfiguredDatasourceListResponse,
|
|
FetchUsersApplicationsWorkspacesResponse,
|
|
ForkApplicationRequest,
|
|
ImportApplicationRequest,
|
|
WorkspaceApplicationObject,
|
|
PublishApplicationRequest,
|
|
PublishApplicationResponse,
|
|
SetDefaultPageRequest,
|
|
UpdateApplicationRequest,
|
|
UpdateApplicationResponse,
|
|
} from "api/ApplicationApi";
|
|
import ApplicationApi from "api/ApplicationApi";
|
|
import { all, call, put, select, takeLatest } from "redux-saga/effects";
|
|
|
|
import { validateResponse } from "./ErrorSagas";
|
|
import { getUserApplicationsWorkspacesList } from "selectors/applicationSelectors";
|
|
import type { ApiResponse } from "api/ApiResponses";
|
|
import history from "utils/history";
|
|
import type { AppState } from "@appsmith/reducers";
|
|
import {
|
|
ApplicationVersion,
|
|
fetchApplication,
|
|
getAllApplications,
|
|
importApplicationSuccess,
|
|
initDatasourceConnectionDuringImportSuccess,
|
|
resetCurrentApplication,
|
|
setDefaultApplicationPageSuccess,
|
|
setIsReconnectingDatasourcesModalOpen,
|
|
setPageIdForImport,
|
|
setWorkspaceIdForImport,
|
|
showReconnectDatasourceModal,
|
|
updateCurrentApplicationEmbedSetting,
|
|
updateCurrentApplicationIcon,
|
|
updateApplicationNavigationSettingAction,
|
|
} from "actions/applicationActions";
|
|
import AnalyticsUtil from "utils/AnalyticsUtil";
|
|
import {
|
|
createMessage,
|
|
DELETING_APPLICATION,
|
|
DISCARD_SUCCESS,
|
|
DUPLICATING_APPLICATION,
|
|
} from "@appsmith/constants/messages";
|
|
import { Toaster, Variant } from "design-system-old";
|
|
import { APP_MODE } from "entities/App";
|
|
import type {
|
|
Workspace,
|
|
Workspaces,
|
|
} from "@appsmith/constants/workspaceConstants";
|
|
import type { AppIconName } from "design-system-old";
|
|
import type { AppColorCode } from "constants/DefaultTheme";
|
|
import {
|
|
getCurrentApplicationId,
|
|
getCurrentPageId,
|
|
} from "selectors/editorSelectors";
|
|
|
|
import {
|
|
deleteRecentAppEntities,
|
|
setPostWelcomeTourState,
|
|
} from "utils/storage";
|
|
import {
|
|
reconnectAppLevelWebsocket,
|
|
reconnectPageLevelWebsocket,
|
|
} from "actions/websocketActions";
|
|
import { getCurrentWorkspace } from "@appsmith/selectors/workspaceSelectors";
|
|
|
|
import {
|
|
getCurrentStep,
|
|
getEnableFirstTimeUserOnboarding,
|
|
getFirstTimeUserOnboardingApplicationId,
|
|
inGuidedTour,
|
|
} from "selectors/onboardingSelectors";
|
|
import { fetchPluginFormConfigs, fetchPlugins } from "actions/pluginActions";
|
|
import {
|
|
fetchDatasources,
|
|
setUnconfiguredDatasourcesDuringImport,
|
|
} from "actions/datasourceActions";
|
|
import { failFastApiCalls } from "./InitSagas";
|
|
import type { Datasource } from "entities/Datasource";
|
|
import { GUIDED_TOUR_STEPS } from "pages/Editor/GuidedTour/constants";
|
|
import { builderURL, viewerURL } from "RouteBuilder";
|
|
import { getDefaultPageId as selectDefaultPageId } from "./selectors";
|
|
import PageApi from "api/PageApi";
|
|
import { identity, merge, pickBy } from "lodash";
|
|
import { checkAndGetPluginFormConfigsSaga } from "./PluginSagas";
|
|
import { getPageList, getPluginForm } from "selectors/entitiesSelector";
|
|
import { getConfigInitialValues } from "components/formControls/utils";
|
|
import DatasourcesApi from "api/DatasourcesApi";
|
|
import { resetApplicationWidgets } from "actions/pageActions";
|
|
import { setCanvasCardsState } from "actions/editorActions";
|
|
import type { User } from "constants/userConstants";
|
|
import { ANONYMOUS_USERNAME } from "constants/userConstants";
|
|
import { getCurrentUser } from "selectors/usersSelectors";
|
|
import { ERROR_CODES } from "@appsmith/constants/ApiConstants";
|
|
|
|
export const getDefaultPageId = (
|
|
pages?: ApplicationPagePayload[],
|
|
): string | undefined => {
|
|
let defaultPage: ApplicationPagePayload | undefined = undefined;
|
|
if (pages) {
|
|
defaultPage = pages.find((page) => page.isDefault);
|
|
if (!defaultPage) {
|
|
defaultPage = pages[0];
|
|
}
|
|
}
|
|
return defaultPage ? defaultPage.id : undefined;
|
|
};
|
|
|
|
let windowReference: Window | null = null;
|
|
|
|
export function* publishApplicationSaga(
|
|
requestAction: ReduxAction<PublishApplicationRequest>,
|
|
) {
|
|
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 currentPageId: string = yield select(getCurrentPageId);
|
|
const guidedTour: boolean = yield select(inGuidedTour);
|
|
const currentStep: number = yield select(getCurrentStep);
|
|
|
|
let appicationViewPageUrl = viewerURL({
|
|
pageId: currentPageId,
|
|
});
|
|
if (guidedTour && currentStep === GUIDED_TOUR_STEPS.DEPLOY) {
|
|
appicationViewPageUrl += "?&guidedTourComplete=true";
|
|
yield call(setPostWelcomeTourState, true);
|
|
}
|
|
|
|
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* getAllApplicationSaga() {
|
|
try {
|
|
const response: FetchUsersApplicationsWorkspacesResponse = yield call(
|
|
ApplicationApi.getAllApplication,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
const workspaceApplication: WorkspaceApplicationObject[] =
|
|
response.data.workspaceApplications.map(
|
|
(userWorkspaces: WorkspaceApplicationObject) => ({
|
|
workspace: userWorkspaces.workspace,
|
|
users: userWorkspaces.users,
|
|
applications: !userWorkspaces.applications
|
|
? []
|
|
: userWorkspaces.applications.map(
|
|
(application: ApplicationObject) => {
|
|
return {
|
|
...application,
|
|
defaultPageId: getDefaultPageId(application.pages),
|
|
};
|
|
},
|
|
),
|
|
}),
|
|
);
|
|
|
|
yield put({
|
|
type: ReduxActionTypes.FETCH_USER_APPLICATIONS_WORKSPACES_SUCCESS,
|
|
payload: workspaceApplication,
|
|
});
|
|
}
|
|
yield call(fetchReleases);
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.FETCH_USER_APPLICATIONS_WORKSPACES_ERROR,
|
|
payload: {
|
|
error,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
export function* fetchAppAndPagesSaga(
|
|
action: ReduxAction<FetchApplicationPayload>,
|
|
) {
|
|
try {
|
|
const params = pickBy(action.payload, identity);
|
|
if (params.pageId && params.applicationId) {
|
|
delete params.applicationId;
|
|
}
|
|
const response: FetchApplicationResponse = yield call(
|
|
PageApi.fetchAppAndPages,
|
|
params,
|
|
);
|
|
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,
|
|
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,
|
|
},
|
|
});
|
|
|
|
yield put({
|
|
type: ReduxActionTypes.SET_CURRENT_WORKSPACE_ID,
|
|
payload: {
|
|
workspaceId: response.data.workspaceId,
|
|
},
|
|
});
|
|
|
|
if (localStorage.getItem("GIT_DISCARD_CHANGES") === "success") {
|
|
Toaster.show({
|
|
text: createMessage(DISCARD_SUCCESS),
|
|
variant: Variant.success,
|
|
});
|
|
localStorage.setItem("GIT_DISCARD_CHANGES", "");
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
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({
|
|
type: ReduxActionTypes.SAFE_CRASH_APPSMITH_REQUEST,
|
|
payload: {
|
|
code: 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<SetDefaultPageRequest>,
|
|
) {
|
|
try {
|
|
const defaultPageId: string = yield select(selectDefaultPageId);
|
|
if (defaultPageId !== action.payload.id) {
|
|
const request: SetDefaultPageRequest = action.payload;
|
|
const response: ApiResponse = yield call(
|
|
ApplicationApi.setDefaultApplicationPage,
|
|
request,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
yield put(
|
|
setDefaultApplicationPageSuccess(request.id, request.applicationId),
|
|
);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.SET_DEFAULT_APPLICATION_PAGE_ERROR,
|
|
payload: {
|
|
error,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
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 (
|
|
request.applicationDetail?.navigationSetting &&
|
|
response.data.applicationDetail?.navigationSetting
|
|
) {
|
|
yield put(
|
|
updateApplicationNavigationSettingAction(
|
|
response.data.applicationDetail.navigationSetting,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.UPDATE_APPLICATION_ERROR,
|
|
payload: {
|
|
error,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
export function* deleteApplicationSaga(
|
|
action: ReduxAction<DeleteApplicationRequest>,
|
|
) {
|
|
try {
|
|
Toaster.show({
|
|
text: createMessage(DELETING_APPLICATION),
|
|
});
|
|
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* duplicateApplicationSaga(
|
|
action: ReduxAction<DeleteApplicationRequest>,
|
|
) {
|
|
try {
|
|
Toaster.show({
|
|
text: createMessage(DUPLICATING_APPLICATION),
|
|
});
|
|
const request: DuplicateApplicationRequest = action.payload;
|
|
const response: ApiResponse = yield call(
|
|
ApplicationApi.duplicateApplication,
|
|
request,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
const application: ApplicationPayload = {
|
|
// @ts-expect-error: response is of type unknown
|
|
...response.data,
|
|
// @ts-expect-error: response is of type unknown
|
|
defaultPageId: getDefaultPageId(response.data.pages),
|
|
};
|
|
yield put({
|
|
type: ReduxActionTypes.DUPLICATE_APPLICATION_SUCCESS,
|
|
payload: response.data,
|
|
});
|
|
|
|
const pageURL = builderURL({
|
|
pageId: application.defaultPageId,
|
|
});
|
|
history.push(pageURL);
|
|
}
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.DUPLICATE_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: AppIconName;
|
|
color: AppColorCode;
|
|
workspaceId: string;
|
|
resolve: any;
|
|
reject: any;
|
|
}>,
|
|
) {
|
|
const { applicationName, color, icon, reject, workspaceId } = action.payload;
|
|
try {
|
|
const userWorkspaces: Workspaces[] = yield select(
|
|
getUserApplicationsWorkspacesList,
|
|
);
|
|
const existingWorkspaces = userWorkspaces.filter(
|
|
(workspace: Workspaces) => workspace.workspace.id === workspaceId,
|
|
)[0];
|
|
const existingApplication = existingWorkspaces
|
|
? existingWorkspaces.applications.find(
|
|
(application: ApplicationPayload) =>
|
|
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,
|
|
};
|
|
const response: CreateApplicationResponse = yield call(
|
|
ApplicationApi.createApplication,
|
|
request,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
const application: ApplicationPayload = {
|
|
...response.data,
|
|
defaultPageId: getDefaultPageId(response.data.pages) as string,
|
|
};
|
|
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,
|
|
},
|
|
});
|
|
const isFirstTimeUserOnboardingEnabled: boolean = yield select(
|
|
getEnableFirstTimeUserOnboarding,
|
|
);
|
|
const FirstTimeUserOnboardingApplicationId: string = yield select(
|
|
getFirstTimeUserOnboardingApplicationId,
|
|
);
|
|
if (
|
|
isFirstTimeUserOnboardingEnabled &&
|
|
FirstTimeUserOnboardingApplicationId === ""
|
|
) {
|
|
yield put({
|
|
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
|
|
payload: application.id,
|
|
});
|
|
}
|
|
// Show cta's in empty canvas for the first page
|
|
yield put(
|
|
setCanvasCardsState(getDefaultPageId(response.data.pages) ?? ""),
|
|
);
|
|
history.push(
|
|
builderURL({
|
|
pageId: application.defaultPageId as string,
|
|
}),
|
|
);
|
|
|
|
// subscribe to newly created application
|
|
// users join rooms on connection, so reconnecting
|
|
// ensures user receives the updates in the app just created
|
|
yield put(reconnectAppLevelWebsocket());
|
|
yield put(reconnectPageLevelWebsocket());
|
|
}
|
|
}
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.CREATE_APPLICATION_ERROR,
|
|
payload: {
|
|
error,
|
|
show: false,
|
|
workspaceId,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
export function* forkApplicationSaga(
|
|
action: ReduxAction<ForkApplicationRequest>,
|
|
) {
|
|
try {
|
|
const response: ApiResponse = yield call(
|
|
ApplicationApi.forkApplication,
|
|
action.payload,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
yield put(resetCurrentApplication());
|
|
const application: ApplicationPayload = {
|
|
// @ts-expect-error: response is of type unknown
|
|
...response.data,
|
|
// @ts-expect-error: response is of type unknown
|
|
defaultPageId: getDefaultPageId(response.data.pages),
|
|
};
|
|
yield put({
|
|
type: ReduxActionTypes.FORK_APPLICATION_SUCCESS,
|
|
payload: {
|
|
workspaceId: action.payload.workspaceId,
|
|
application,
|
|
},
|
|
});
|
|
yield put({
|
|
type: ReduxActionTypes.SET_CURRENT_WORKSPACE_ID,
|
|
payload: {
|
|
id: action.payload.workspaceId,
|
|
},
|
|
});
|
|
const pageURL = builderURL({
|
|
pageId: application.defaultPageId as string,
|
|
});
|
|
history.push(pageURL);
|
|
}
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.FORK_APPLICATION_ERROR,
|
|
payload: {
|
|
error,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function* showReconnectDatasourcesModalSaga(
|
|
action: ReduxAction<{
|
|
application: ApplicationResponsePayload;
|
|
unConfiguredDatasourceList: Array<Datasource>;
|
|
workspaceId: string;
|
|
pageId?: string;
|
|
}>,
|
|
) {
|
|
const { application, pageId, unConfiguredDatasourceList, workspaceId } =
|
|
action.payload;
|
|
yield put(getAllApplications());
|
|
yield put(importApplicationSuccess(application));
|
|
yield put(fetchPlugins({ workspaceId }));
|
|
|
|
yield put(
|
|
setUnconfiguredDatasourcesDuringImport(unConfiguredDatasourceList || []),
|
|
);
|
|
|
|
yield put(setWorkspaceIdForImport(workspaceId));
|
|
yield put(setPageIdForImport(pageId));
|
|
yield put(setIsReconnectingDatasourcesModalOpen({ isOpen: true }));
|
|
}
|
|
|
|
export function* importApplicationSaga(
|
|
action: ReduxAction<ImportApplicationRequest>,
|
|
) {
|
|
try {
|
|
const response: ApiResponse = yield call(
|
|
ApplicationApi.importApplicationToWorkspace,
|
|
action.payload,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
const allWorkspaces: Workspace[] = yield select(getCurrentWorkspace);
|
|
const currentWorkspace = allWorkspaces.filter(
|
|
(el: Workspace) => el.id === action.payload.workspaceId,
|
|
);
|
|
if (currentWorkspace.length > 0) {
|
|
const {
|
|
// @ts-expect-error: response is of type unknown
|
|
application: { pages },
|
|
// @ts-expect-error: response is of type unknown
|
|
isPartialImport,
|
|
} = response.data;
|
|
|
|
// @ts-expect-error: response is of type unknown
|
|
yield put(importApplicationSuccess(response.data?.application));
|
|
|
|
if (isPartialImport) {
|
|
yield put(
|
|
showReconnectDatasourceModal({
|
|
// @ts-expect-error: response is of type unknown
|
|
application: response.data?.application,
|
|
unConfiguredDatasourceList:
|
|
// @ts-expect-error: response is of type unknown
|
|
response?.data.unConfiguredDatasourceList,
|
|
workspaceId: action.payload.workspaceId,
|
|
}),
|
|
);
|
|
} else {
|
|
// @ts-expect-error: pages is of type any
|
|
// TODO: Update route params here
|
|
const defaultPage = pages.filter((eachPage) => !!eachPage.isDefault);
|
|
const pageURL = builderURL({
|
|
pageId: defaultPage[0].id,
|
|
});
|
|
history.push(pageURL);
|
|
const guidedTour: boolean = yield select(inGuidedTour);
|
|
|
|
if (guidedTour) return;
|
|
|
|
Toaster.show({
|
|
text: "Application imported successfully",
|
|
variant: Variant.success,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
yield put({
|
|
type: ReduxActionErrorTypes.IMPORT_APPLICATION_ERROR,
|
|
payload: {
|
|
error,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if (!datasource.datasourceConfiguration) {
|
|
yield call(checkAndGetPluginFormConfigsSaga, datasource.pluginId);
|
|
const formConfig: Record<string, unknown>[] = yield select(
|
|
getPluginForm,
|
|
datasource.pluginId,
|
|
);
|
|
const initialValues: unknown = yield call(
|
|
getConfigInitialValues,
|
|
formConfig,
|
|
);
|
|
const payload = merge(initialValues, datasource);
|
|
payload.isConfigured = false; // imported datasource as not configured yet
|
|
const response: ApiResponse = yield DatasourcesApi.updateDatasource(
|
|
payload,
|
|
datasource.id,
|
|
);
|
|
const isValidResponse: boolean = yield validateResponse(response);
|
|
if (isValidResponse) {
|
|
yield put({
|
|
type: ReduxActionTypes.UPDATE_DATASOURCE_IMPORT_SUCCESS,
|
|
payload: response.data,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function* initDatasourceConnectionDuringImport(action: ReduxAction<string>) {
|
|
const workspaceId = action.payload;
|
|
|
|
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: AppState) => {
|
|
return state.entities.datasources.list;
|
|
});
|
|
|
|
yield all(
|
|
datasources.map((datasource: Datasource) =>
|
|
call(initializeDatasourceWithDefaultValues, datasource),
|
|
),
|
|
);
|
|
|
|
yield put(initDatasourceConnectionDuringImportSuccess());
|
|
}
|
|
|
|
export default function* applicationSagas() {
|
|
yield all([
|
|
takeLatest(
|
|
ReduxActionTypes.PUBLISH_APPLICATION_INIT,
|
|
publishApplicationSaga,
|
|
),
|
|
takeLatest(ReduxActionTypes.UPDATE_APP_LAYOUT, updateApplicationLayoutSaga),
|
|
takeLatest(ReduxActionTypes.UPDATE_APPLICATION, updateApplicationSaga),
|
|
takeLatest(
|
|
ReduxActionTypes.CHANGE_APPVIEW_ACCESS_INIT,
|
|
changeAppViewAccessSaga,
|
|
),
|
|
takeLatest(
|
|
ReduxActionTypes.GET_ALL_APPLICATION_INIT,
|
|
getAllApplicationSaga,
|
|
),
|
|
takeLatest(ReduxActionTypes.FETCH_APPLICATION_INIT, fetchAppAndPagesSaga),
|
|
takeLatest(ReduxActionTypes.FORK_APPLICATION_INIT, forkApplicationSaga),
|
|
takeLatest(ReduxActionTypes.CREATE_APPLICATION_INIT, createApplicationSaga),
|
|
takeLatest(
|
|
ReduxActionTypes.SET_DEFAULT_APPLICATION_PAGE_INIT,
|
|
setDefaultApplicationPageSaga,
|
|
),
|
|
takeLatest(ReduxActionTypes.DELETE_APPLICATION_INIT, deleteApplicationSaga),
|
|
takeLatest(
|
|
ReduxActionTypes.DUPLICATE_APPLICATION_INIT,
|
|
duplicateApplicationSaga,
|
|
),
|
|
takeLatest(ReduxActionTypes.IMPORT_APPLICATION_INIT, importApplicationSaga),
|
|
takeLatest(ReduxActionTypes.FETCH_RELEASES, fetchReleases),
|
|
takeLatest(
|
|
ReduxActionTypes.INIT_DATASOURCE_CONNECTION_DURING_IMPORT_REQUEST,
|
|
initDatasourceConnectionDuringImport,
|
|
),
|
|
takeLatest(
|
|
ReduxActionTypes.SHOW_RECONNECT_DATASOURCE_MODAL,
|
|
showReconnectDatasourcesModalSaga,
|
|
),
|
|
takeLatest(
|
|
ReduxActionTypes.FETCH_UNCONFIGURED_DATASOURCE_LIST,
|
|
fetchUnconfiguredDatasourceList,
|
|
),
|
|
]);
|
|
}
|