PromucFlow_constructor/app/client/src/sagas/OnboardingSagas.ts
Tanvi Bhakta ac0c872843
chore: migrate toast (#17208)
* Refactor toast to be passed the dispatch hook externally

* Add comments explaining dilemma

* use store.dispatch instead of a hook

* use alpha version

* Change imports

* Refactor DebugButton out

* update release

* fix issue with incorrectly merged package.lock

* fix syntax of alpha version

* bump ds vesion

* copy lock from release

* update lock to have alpha

* make changes

* delete Toast

* DS package version updated

* import change from release

* use new alpha version

* update ds version

* update ds version

* chore: migrate editable text and friends (#17285)

* Delete empty components

* use alpha for ds

* Deleted EditableTextSubComponent, import changes

* Delete EditableText, import changes

* use ds alpha 10

* Delete EditableTextWrapper.tsx

* update ds to use next minor version

* use new alpha

* fix issue with merge

Co-authored-by: Albin <albin@appsmith.com>

* chore: migrate file picker v2 (#17308)

* use alpha ds

* Delete FilePickerV2, import changes

* Delete FilePicker, change imports

* update alpha version

* chore: move copy url form into setting components (#17322)

* move CopyUrlForm to src/pages/settings/formgroup

* update ds version to use next minor release

* feat: Migrate table component to design system (#17329)

* feat: Migrate table component to design system

* removed commented code in ads index file

* fix: table no data hover effect removed

Co-authored-by: Tanvi Bhakta <tanvibhakta@gmail.com>

* feat: Banner message component migrated to design system (#17327)

* feat: Banner image component migrated to design system

* Version update for design system package

* design system version updated

Co-authored-by: Tanvi Bhakta <tanvibhakta@gmail.com>

* feat: Tabs component migrated to design system (#17321)

* feat: Tabs component migrated to design system

* design system package version updated

* Update app/client/src/components/editorComponents/form/FormDialogComponent.tsx

* Update app/client/src/pages/Editor/PropertyPane/PropertyPaneTab.tsx

* Tab component expand issue fix

Co-authored-by: Tanvi Bhakta <tanvibhakta@gmail.com>

Co-authored-by: Albin <albin@appsmith.com>
Co-authored-by: albinAppsmith <87797149+albinAppsmith@users.noreply.github.com>
2022-10-14 01:43:44 +05:30

497 lines
15 KiB
TypeScript

import {
ReduxAction,
ReduxActionTypes,
WidgetReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import {
all,
put,
select,
takeLatest,
delay,
call,
take,
} from "redux-saga/effects";
import {
setEnableFirstTimeUserOnboarding as storeEnableFirstTimeUserOnboarding,
setFirstTimeUserOnboardingApplicationId as storeFirstTimeUserOnboardingApplicationId,
setFirstTimeUserOnboardingIntroModalVisibility as storeFirstTimeUserOnboardingIntroModalVisibility,
} from "utils/storage";
import { getCurrentUser } from "selectors/usersSelectors";
import history from "utils/history";
import TourApp from "pages/Editor/GuidedTour/app.json";
import {
getFirstTimeUserOnboardingApplicationId,
getHadReachedStep,
getOnboardingWorkspaces,
getQueryAction,
getTableWidget,
} from "selectors/onboardingSelectors";
import { Toaster } from "design-system";
import { Variant } from "components/ads/common";
import { Workspaces } from "constants/workspaceConstants";
import {
enableGuidedTour,
focusWidgetProperty,
loadGuidedTour,
setCurrentStep,
toggleLoader,
} from "actions/onboardingActions";
import {
getCurrentApplicationId,
getCurrentPageId,
getIsEditorInitialized,
} from "selectors/editorSelectors";
import { WidgetProps } from "widgets/BaseWidget";
import { getNextWidgetName } from "./WidgetOperationUtils";
import WidgetFactory from "utils/WidgetFactory";
import { generateReactKey } from "utils/generators";
import { RenderModes } from "constants/WidgetConstants";
import log from "loglevel";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getWidgets } from "./selectors";
import { clearActionResponse } from "actions/pluginActionActions";
import {
importApplication,
updateApplicationLayout,
} from "actions/applicationActions";
import { setPreviewModeAction } from "actions/editorActions";
import { FlattenedWidgetProps } from "widgets/constants";
import { ActionData } from "reducers/entityReducers/actionsReducer";
import { batchUpdateMultipleWidgetProperties } from "actions/controlActions";
import {
setExplorerActiveAction,
setExplorerPinnedAction,
} from "actions/explorerActions";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import { hideIndicator } from "pages/Editor/GuidedTour/utils";
import { updateWidgetName } from "actions/propertyPaneActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { DataTree } from "entities/DataTree/dataTreeFactory";
import { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import { User } from "constants/userConstants";
import { builderURL, queryEditorIdURL } from "RouteBuilder";
import { GuidedTourEntityNames } from "pages/Editor/GuidedTour/constants";
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/utils";
import { shouldBeDefined } from "utils/helpers";
import { GuidedTourState } from "reducers/uiReducers/guidedTourReducer";
import { sessionStorage } from "utils/localStorage";
import store from "store";
import {
createMessage,
ONBOARDING_SKIPPED_FIRST_TIME_USER,
} from "@appsmith/constants/messages";
const GUIDED_TOUR_STORAGE_KEY = "GUIDED_TOUR_STORAGE_KEY";
function* createApplication() {
// If we are starting onboarding from the editor wait for the editor to reset.
const isEditorInitialised: boolean = yield select(getIsEditorInitialized);
let userWorkspaces: Workspaces[] = yield select(getOnboardingWorkspaces);
if (isEditorInitialised) {
yield take(ReduxActionTypes.RESET_EDITOR_SUCCESS);
// If we haven't fetched the workspace list yet we wait for it to complete
// as we need an workspace where we create an application
if (!userWorkspaces.length) {
yield take(ReduxActionTypes.FETCH_USER_APPLICATIONS_WORKSPACES_SUCCESS);
}
}
userWorkspaces = yield select(getOnboardingWorkspaces);
const currentUser: User | undefined = yield select(getCurrentUser);
// @ts-expect-error: currentUser can be undefined
const currentWorkspaceId = currentUser.currentWorkspaceId;
let workspace;
if (!currentWorkspaceId) {
workspace = userWorkspaces[0];
} else {
const filteredWorkspaces = userWorkspaces.filter(
(workspace: any) => workspace.workspace.id === currentWorkspaceId,
);
workspace = filteredWorkspaces[0];
}
if (workspace) {
const appFileObject = new File([JSON.stringify(TourApp)], "app.json", {
type: "application/json",
});
yield put(enableGuidedTour(true));
yield put(
importApplication({
workspaceId: workspace.workspace.id,
applicationFile: appFileObject,
}),
);
}
yield put(setPreviewModeAction(true));
}
function* syncGuidedTourStateSaga() {
const applicationId: string = yield select(getCurrentApplicationId);
const guidedTourState: GuidedTourState = yield select(
(state) => state.ui.guidedTour,
);
yield call(
sessionStorage.setItem,
GUIDED_TOUR_STORAGE_KEY,
JSON.stringify({ applicationId, guidedTourState }),
);
}
function* loadGuidedTourInitSaga() {
const applicationId: string = yield select(getCurrentApplicationId);
const guidedTourState: undefined | string = yield call(
sessionStorage.getItem,
GUIDED_TOUR_STORAGE_KEY,
);
if (guidedTourState) {
const parsedGuidedTourState: {
applicationId: string;
guidedTourState: GuidedTourState;
} = JSON.parse(guidedTourState);
if (applicationId === parsedGuidedTourState.applicationId) {
yield put(loadGuidedTour(parsedGuidedTourState.guidedTourState));
}
}
}
function* setCurrentStepSaga(action: ReduxAction<number>) {
const hadReachedStep: number = yield select(getHadReachedStep);
// Log only once when we reach that step
if (action.payload > hadReachedStep) {
AnalyticsUtil.logEvent("GUIDED_TOUR_REACHED_STEP", {
step: action.payload,
});
}
yield call(syncGuidedTourStateSaga);
yield put(setCurrentStep(action.payload));
}
function* setUpTourAppSaga() {
yield put(setPreviewModeAction(false));
// Delete the container widget
const widgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const containerWidget = Object.values(widgets).find(
(widget) => widget.type === "CONTAINER_WIDGET",
);
yield put({
type: WidgetReduxActionTypes.WIDGET_DELETE,
payload: {
widgetId: containerWidget?.widgetId,
parentId: containerWidget?.parentId,
disallowUndo: true,
},
});
yield delay(500);
// @ts-expect-error: No type declared for getTableWidgetSelector.
const tableWidget = yield select(getTableWidget);
yield put(
batchUpdateMultipleWidgetProperties([
{
widgetId: tableWidget.widgetId,
updates: {
modify: {
tableData: "",
},
},
},
]),
);
// Update getCustomers query body
const query: ActionData | undefined = yield select(getQueryAction);
yield put(clearActionResponse(query?.config.id ?? ""));
const applicationId: string = yield select(getCurrentApplicationId);
yield put(
updateApplicationLayout(applicationId || "", {
appLayout: {
type: "DESKTOP",
},
}),
);
// Hide the explorer initialy
yield put(setExplorerPinnedAction(false));
yield put(setExplorerActiveAction(false));
yield put(toggleLoader(false));
if (!query) return;
history.push(
queryEditorIdURL({
pageId: query.config.pageId,
queryId: query.config.id,
}),
);
}
function* addOnboardingWidget(action: ReduxAction<Partial<WidgetProps>>) {
const widgetConfig = action.payload;
if (!widgetConfig.type) return;
const defaultConfig = WidgetFactory.widgetConfigMap.get(widgetConfig.type);
const evalTree: DataTree = yield select(getDataTree);
const widgets: CanvasWidgetsReduxState = yield select(getWidgets);
const widgetName = getNextWidgetName(widgets, widgetConfig.type, evalTree, {
prefix: widgetConfig.widgetName,
});
try {
const newWidget = {
newWidgetId: generateReactKey(),
widgetId: "0",
parentId: "0",
renderMode: RenderModes.CANVAS,
isLoading: false,
...defaultConfig,
widgetName,
...widgetConfig,
};
yield put({
type: WidgetReduxActionTypes.WIDGET_ADD_CHILD,
payload: newWidget,
});
// Wait for widget names to be updated
// Updating widget names here as widget blueprints don't take widget names
yield take(ReduxActionTypes.SAVE_PAGE_SUCCESS);
const widgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const nameInput = Object.values(widgets).find(
(widget) => widget.widgetName === "Input1",
);
const emailInput = Object.values(widgets).find(
(widget) => widget.widgetName === "Input2",
);
const countryInput = Object.values(widgets).find(
(widget) => widget.widgetName === "Input3",
);
const imageWidget = Object.values(widgets).find(
(widget) => widget.widgetName === "Image1",
);
if (nameInput && emailInput && countryInput && imageWidget) {
yield put(
updateWidgetName(nameInput.widgetId, GuidedTourEntityNames.NAME_INPUT),
);
yield take(ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS);
yield put(
updateWidgetName(
emailInput.widgetId,
GuidedTourEntityNames.EMAIL_INPUT,
),
);
yield take(ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS);
yield put(
updateWidgetName(
countryInput.widgetId,
GuidedTourEntityNames.COUNTRY_INPUT,
),
);
yield take(ReduxActionTypes.FETCH_PAGE_DSL_SUCCESS);
yield put(
updateWidgetName(
imageWidget.widgetId,
GuidedTourEntityNames.DISPLAY_IMAGE,
),
);
}
} catch (error) {
log.error(error);
}
}
// Update button widget text
function* updateWidgetTextSaga() {
const widgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const buttonWidget = Object.values(widgets).find(
(widget) => widget.type === "BUTTON_WIDGET",
);
if (buttonWidget) {
yield put(
batchUpdateMultipleWidgetProperties([
{
widgetId: buttonWidget.widgetId,
updates: {
modify: {
text: "Click to Update",
rightColumn: buttonWidget.leftColumn + 24,
bottomRow: buttonWidget.topRow + 5,
widgetName: GuidedTourEntityNames.BUTTON_WIDGET,
},
},
},
]),
);
}
}
function* focusWidgetPropertySaga(action: ReduxAction<string>) {
const input: HTMLElement | null = document.querySelector(
`[data-guided-tour-iid=${action.payload}] .CodeEditorTarget textarea`,
);
input?.focus();
}
function* endGuidedTourSaga(action: ReduxAction<boolean>) {
if (!action.payload) {
yield call(hideIndicator);
yield call(sessionStorage.removeItem, GUIDED_TOUR_STORAGE_KEY);
}
}
function* selectWidgetSaga(
action: ReduxAction<{ widgetName: string; propertyName?: string }>,
) {
const widgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
getWidgets,
);
const pageId = shouldBeDefined<string>(
yield select(getCurrentPageId),
"Page not found in state.entities.pageList.currentPageId",
);
const widget = Object.values(widgets).find((widget) => {
return widget.widgetName === action.payload.widgetName;
});
if (widget) {
// Navigate to the widget as well, usefull especially when we are not on the canvas
navigateToCanvas({ pageId, widgetId: widget.widgetId });
yield put(selectWidgetInitAction(widget.widgetId));
// Delay to wait for the fields to render
yield delay(1000);
// If the propertyName exist then we focus the respective input field as well
if (action.payload.propertyName)
yield put(focusWidgetProperty(action.payload.propertyName));
}
}
// Signposting sagas
function* setEnableFirstTimeUserOnboarding(action: ReduxAction<boolean>) {
yield storeEnableFirstTimeUserOnboarding(action.payload);
}
function* setFirstTimeUserOnboardingApplicationId(action: ReduxAction<string>) {
yield storeFirstTimeUserOnboardingApplicationId(action.payload);
}
function* setFirstTimeUserOnboardingIntroModalVisibility(
action: ReduxAction<boolean>,
) {
yield storeFirstTimeUserOnboardingIntroModalVisibility(action.payload);
}
function* endFirstTimeUserOnboardingSaga() {
const firstTimeUserExperienceAppId: string = yield select(
getFirstTimeUserOnboardingApplicationId,
);
yield put({
type: ReduxActionTypes.SET_ENABLE_FIRST_TIME_USER_ONBOARDING,
payload: false,
});
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
payload: "",
});
Toaster.show({
text: createMessage(ONBOARDING_SKIPPED_FIRST_TIME_USER),
hideProgressBar: false,
variant: Variant.success,
dispatchableAction: {
dispatch: store.dispatch,
type: ReduxActionTypes.UNDO_END_FIRST_TIME_USER_ONBOARDING,
payload: firstTimeUserExperienceAppId,
},
});
}
function* undoEndFirstTimeUserOnboardingSaga(action: ReduxAction<string>) {
yield put({
type: ReduxActionTypes.SET_ENABLE_FIRST_TIME_USER_ONBOARDING,
payload: true,
});
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
payload: action.payload,
});
}
function* firstTimeUserOnboardingInitSaga(
action: ReduxAction<{ applicationId: string; pageId: string }>,
) {
yield put({
type: ReduxActionTypes.SET_ENABLE_FIRST_TIME_USER_ONBOARDING,
payload: true,
});
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
payload: action.payload.applicationId,
});
yield put({
type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL,
payload: true,
});
history.replace(
builderURL({
pageId: action.payload.pageId,
}),
);
}
export default function* onboardingActionSagas() {
yield all([
takeLatest(
ReduxActionTypes.ONBOARDING_CREATE_APPLICATION,
createApplication,
),
takeLatest(ReduxActionTypes.SET_UP_TOUR_APP, setUpTourAppSaga),
takeLatest(ReduxActionTypes.GUIDED_TOUR_ADD_WIDGET, addOnboardingWidget),
takeLatest(ReduxActionTypes.SET_CURRENT_STEP_INIT, setCurrentStepSaga),
takeLatest(
ReduxActionTypes.UPDATE_BUTTON_WIDGET_TEXT,
updateWidgetTextSaga,
),
takeLatest(ReduxActionTypes.ENABLE_GUIDED_TOUR, endGuidedTourSaga),
takeLatest(ReduxActionTypes.GUIDED_TOUR_FOCUS_WIDGET, selectWidgetSaga),
takeLatest(ReduxActionTypes.FOCUS_WIDGET_PROPERTY, focusWidgetPropertySaga),
takeLatest(ReduxActionTypes.LOAD_GUIDED_TOUR_INIT, loadGuidedTourInitSaga),
takeLatest(
ReduxActionTypes.SET_ENABLE_FIRST_TIME_USER_ONBOARDING,
setEnableFirstTimeUserOnboarding,
),
takeLatest(
ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
setFirstTimeUserOnboardingApplicationId,
),
takeLatest(
ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL,
setFirstTimeUserOnboardingIntroModalVisibility,
),
takeLatest(
ReduxActionTypes.END_FIRST_TIME_USER_ONBOARDING,
endFirstTimeUserOnboardingSaga,
),
takeLatest(
ReduxActionTypes.UNDO_END_FIRST_TIME_USER_ONBOARDING,
undoEndFirstTimeUserOnboardingSaga,
),
takeLatest(
ReduxActionTypes.FIRST_TIME_USER_ONBOARDING_INIT,
firstTimeUserOnboardingInitSaga,
),
]);
}