PromucFlow_constructor/app/client/src/sagas/OnboardingSagas.ts
balajisoundar 2608e3dbd3
chore: Move the widget config to widget class (#26073)
## Description
- Remove the config objects from widget and config maps from the widget
factory.
- Introduce methods in widget development API to dynamically fetch this
items.
- freeze the widget configuration.

#### PR fixes following issue(s)
Fixes https://github.com/appsmithorg/appsmith/issues/26008
> if no issue exists, please create an issue and ask the maintainers
about this first
>
>
#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> Add Testsmith test cases links that relate to this PR
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] 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
- [ ] 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:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [x] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [x] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
2023-09-06 17:45:04 +05:30

590 lines
18 KiB
TypeScript

import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import {
ReduxActionTypes,
WidgetReduxActionTypes,
} from "@appsmith/constants/ReduxActionConstants";
import {
all,
call,
delay,
put,
select,
take,
takeLatest,
} from "redux-saga/effects";
import {
getFirstTimeUserOnboardingApplicationIds,
getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown,
removeAllFirstTimeUserOnboardingApplicationIds,
removeFirstTimeUserOnboardingApplicationId,
setEnableStartSignposting,
setFirstTimeUserOnboardingApplicationId as storeFirstTimeUserOnboardingApplicationId,
setFirstTimeUserOnboardingIntroModalVisibility as storeFirstTimeUserOnboardingIntroModalVisibility,
} from "utils/storage";
import { getCurrentUser } from "selectors/usersSelectors";
import history from "utils/history";
import {
getHadReachedStep,
getOnboardingWorkspaces,
getQueryAction,
getSignpostingStepStateByStep,
getTableWidget,
} from "selectors/onboardingSelectors";
import type { Workspaces } from "@appsmith/constants/workspaceConstants";
import {
disableStartSignpostingAction,
enableGuidedTour,
focusWidgetProperty,
loadGuidedTour,
removeFirstTimeUserOnboardingApplicationId as removeFirstTimeUserOnboardingApplicationIdAction,
setCurrentStep,
setSignpostingOverlay,
showSignpostingTooltip,
signpostingStepUpdate,
toggleLoader,
} from "actions/onboardingActions";
import {
getCurrentApplicationId,
getIsEditorInitialized,
} from "selectors/editorSelectors";
import type { WidgetProps } from "widgets/BaseWidget";
import { getNextWidgetName } from "./WidgetOperationUtils";
import WidgetFactory from "WidgetProvider/factory";
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 "@appsmith/actions/applicationActions";
import { setPreviewModeAction } from "actions/editorActions";
import type { FlattenedWidgetProps } from "WidgetProvider/constants";
import type { 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 type { DataTree } from "entities/DataTree/dataTreeFactory";
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import type { User } from "constants/userConstants";
import { builderURL, queryEditorIdURL } from "RouteBuilder";
import { GuidedTourEntityNames } from "pages/Editor/GuidedTour/constants";
import type { GuidedTourState } from "reducers/uiReducers/guidedTourReducer";
import { sessionStorage } from "utils/localStorage";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import type { SIGNPOSTING_STEP } from "pages/Editor/FirstTimeUserOnboarding/Utils";
import type { StepState } from "reducers/uiReducers/onBoardingReducer";
import { isUndefined } from "lodash";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { SIGNPOSTING_ANALYTICS_STEP_NAME } from "pages/Editor/FirstTimeUserOnboarding/constants";
import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
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 TourAppPromise = import("pages/Editor/GuidedTour/app.json");
const TourApp: Awaited<typeof TourAppPromise> = yield TourAppPromise;
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",
},
}),
);
if (!query) return;
history.push(
queryEditorIdURL({
pageId: query.config.pageId,
queryId: query.config.id,
}),
);
// Hide the explorer initialy
yield put(setExplorerPinnedAction(false));
yield put(setExplorerActiveAction(false));
yield put(toggleLoader(false));
}
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 widget = Object.values(widgets).find((widget) => {
return widget.widgetName === action.payload.widgetName;
});
if (widget) {
yield put(
selectWidgetInitAction(SelectionRequestType.One, [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* setFirstTimeUserOnboardingApplicationId(action: ReduxAction<string>) {
yield storeFirstTimeUserOnboardingApplicationId(action.payload);
const applicationIds: string[] =
yield getFirstTimeUserOnboardingApplicationIds();
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_IDS,
payload: [...applicationIds, ...action.payload],
});
}
function* removeFirstTimeUserOnboardingApplicationIdSaga(
action: ReduxAction<string>,
) {
yield call(removeFirstTimeUserOnboardingApplicationId, action.payload);
const applicationIds: string[] =
yield getFirstTimeUserOnboardingApplicationIds();
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_IDS,
payload: applicationIds.filter((id) => id !== action.payload),
});
}
function* setFirstTimeUserOnboardingIntroModalVisibility(
action: ReduxAction<boolean>,
) {
yield storeFirstTimeUserOnboardingIntroModalVisibility(action.payload);
}
function* endFirstTimeUserOnboardingSaga() {
const firstTimeUserExperienceAppId: string = yield select(
getCurrentApplicationId,
);
yield put(
removeFirstTimeUserOnboardingApplicationIdAction(
firstTimeUserExperienceAppId,
),
);
}
function* firstTimeUserOnboardingInitSaga(
action: ReduxAction<{ applicationId: string; pageId: string }>,
) {
yield call(setEnableStartSignposting, true);
yield put({
type: ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
payload: action.payload.applicationId,
});
history.replace(
builderURL({
pageId: action.payload.pageId,
}),
);
const isEditorInitialised: boolean = yield select(getIsEditorInitialized);
if (!isEditorInitialised) {
yield take(ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS);
}
let showOverlay = true;
// We don't want to show the signposting overlay when we intend to show the
// telemetry callout
const currentUser: User | undefined = yield select(getCurrentUser);
if (currentUser?.isSuperUser && !isAirgapped()) {
const isAnonymousDataPopupAlreadyOpen: unknown = yield call(
getFirstTimeUserOnboardingTelemetryCalloutIsAlreadyShown,
);
if (!isAnonymousDataPopupAlreadyOpen) {
showOverlay = false;
}
}
yield put(setSignpostingOverlay(showOverlay));
const tableWidgetExperimentEnabled: boolean = yield select(
selectFeatureFlagCheck,
FEATURE_FLAG.ab_table_widget_activation_enabled,
);
if (tableWidgetExperimentEnabled) {
yield take(ReduxActionTypes.FETCH_WORKSPACE_SUCCESS);
yield put({
type: ReduxActionTypes.WIDGET_ADD_CHILD,
payload: {
widgetId: "0",
type: "TABLE_WIDGET_V2",
leftColumn: 15,
topRow: 6,
columns: 34,
rows: 28,
parentRowSpace: 10,
parentColumnSpace: 13.390625,
newWidgetId: generateReactKey(),
},
});
}
// Show the modal once the editor is loaded. The delay is to grab user attention back once the editor
yield delay(1000);
yield put({
type: ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL,
payload: true,
});
AnalyticsUtil.logEvent("SIGNPOSTING_MODAL_FIRST_TIME_OPEN");
}
function* setFirstTimeUserOnboardingCompleteSaga(action: ReduxAction<boolean>) {
if (action.payload) {
yield put(disableStartSignpostingAction());
}
}
function* disableStartFirstTimeUserOnboardingSaga() {
yield call(removeAllFirstTimeUserOnboardingApplicationIds);
yield call(setEnableStartSignposting, false);
}
function* setSignpostingStepStateSaga(
action: ReduxAction<{ step: SIGNPOSTING_STEP; completed: boolean }>,
) {
const { completed, step } = action.payload;
const stepState: StepState | undefined = yield select(
getSignpostingStepStateByStep,
step,
);
// No changes to update so we ignore
if (stepState && stepState.completed === completed) return;
const readProps = completed
? {
read: false,
}
: {};
yield put(
signpostingStepUpdate({
...action.payload,
...readProps,
}),
);
// Show tooltip when a step is completed
if (!isUndefined(readProps.read) && !readProps.read) {
// Show tooltip after a small delay to not be abrupt
yield delay(1000);
AnalyticsUtil.logEvent("SIGNPOSTING_STEP_COMPLETE", {
step_name: SIGNPOSTING_ANALYTICS_STEP_NAME[step],
});
yield put(showSignpostingTooltip(true));
}
}
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_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
setFirstTimeUserOnboardingApplicationId,
),
takeLatest(
ReduxActionTypes.REMOVE_FIRST_TIME_USER_ONBOARDING_APPLICATION_ID,
removeFirstTimeUserOnboardingApplicationIdSaga,
),
takeLatest(
ReduxActionTypes.SET_SHOW_FIRST_TIME_USER_ONBOARDING_MODAL,
setFirstTimeUserOnboardingIntroModalVisibility,
),
takeLatest(
ReduxActionTypes.END_FIRST_TIME_USER_ONBOARDING,
endFirstTimeUserOnboardingSaga,
),
takeLatest(
ReduxActionTypes.FIRST_TIME_USER_ONBOARDING_INIT,
firstTimeUserOnboardingInitSaga,
),
takeLatest(
ReduxActionTypes.SET_FIRST_TIME_USER_ONBOARDING_COMPLETE,
setFirstTimeUserOnboardingCompleteSaga,
),
takeLatest(
ReduxActionTypes.DISABLE_START_SIGNPOSTING,
disableStartFirstTimeUserOnboardingSaga,
),
takeLatest(
ReduxActionTypes.SIGNPOSTING_STEP_UPDATE_INIT,
setSignpostingStepStateSaga,
),
]);
}