## Description
This PR upgrades Prettier to v2 + enforces TypeScript’s [`import
type`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export)
syntax where applicable. It’s submitted as a separate PR so we can merge
it easily.
As a part of this PR, we reformat the codebase heavily:
- add `import type` everywhere where it’s required, and
- re-format the code to account for Prettier 2’s breaking changes:
https://prettier.io/blog/2020/03/21/2.0.0.html#breaking-changes
This PR is submitted against `release` to make sure all new code by team
members will adhere to new formatting standards, and we’ll have fewer
conflicts when merging `bundle-optimizations` into `release`. (I’ll
merge `release` back into `bundle-optimizations` once this PR is
merged.)
### Why is this needed?
This PR is needed because, for the Lodash optimization from
7cbb12af88,
we need to use `import type`. Otherwise, `babel-plugin-lodash` complains
that `LoDashStatic` is not a lodash function.
However, just using `import type` in the current codebase will give you
this:
<img width="962" alt="Screenshot 2023-03-08 at 17 45 59"
src="https://user-images.githubusercontent.com/2953267/223775744-407afa0c-e8b9-44a1-90f9-b879348da57f.png">
That’s because Prettier 1 can’t parse `import type` at all. To parse it,
we need to upgrade to Prettier 2.
### Why enforce `import type`?
Apart from just enabling `import type` support, this PR enforces
specifying `import type` everywhere it’s needed. (Developers will get
immediate TypeScript and ESLint errors when they forget to do so.)
I’m doing this because I believe `import type` improves DX and makes
refactorings easier.
Let’s say you had a few imports like below. Can you tell which of these
imports will increase the bundle size? (Tip: it’s not all of them!)
```ts
// app/client/src/workers/Linting/utils.ts
import { Position } from "codemirror";
import { LintError as JSHintError, LintOptions } from "jshint";
import { get, isEmpty, isNumber, keys, last, set } from "lodash";
```
It’s pretty hard, right?
What about now?
```ts
// app/client/src/workers/Linting/utils.ts
import type { Position } from "codemirror";
import type { LintError as JSHintError, LintOptions } from "jshint";
import { get, isEmpty, isNumber, keys, last, set } from "lodash";
```
Now, it’s clear that only `lodash` will be bundled.
This helps developers to see which imports are problematic, but it
_also_ helps with refactorings. Now, if you want to see where
`codemirror` is bundled, you can just grep for `import \{.*\} from
"codemirror"` – and you won’t get any type-only imports.
This also helps (some) bundlers. Upon transpiling, TypeScript erases
type-only imports completely. In some environment (not ours), this makes
the bundle smaller, as the bundler doesn’t need to bundle type-only
imports anymore.
## Type of change
- Chore (housekeeping or task changes that don't impact user perception)
## How Has This Been Tested?
This was tested to not break the build.
### 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
- [ ] I have performed a self-review of my own code
- [ ] 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
- [ ] 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: Satish Gandham <hello@satishgandham.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
920 lines
27 KiB
TypeScript
920 lines
27 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,
|
|
} 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),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} 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,
|
|
),
|
|
]);
|
|
}
|