feat: multiple highlight area in walkthrough, corrected walkthrough events for each feature (#25520)

## Description
- Added support for multiple highlights in walkthrough
- Corrected walkthrough events for each feature walkthrough
- Added fix for the walkthrough dismissal in case of empty/no schema
feature

#### PR fixes following issue(s)
Fixes #25417 
Fixes #25416 

#### 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
- [ ] 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
- [ ] 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
- [ ] 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

---------

Co-authored-by: “sneha122” <“sneha@appsmith.com”>
This commit is contained in:
Aman Agarwal 2023-07-27 18:30:23 +05:30 committed by GitHub
parent 2f36589171
commit e3e75acc32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 312 additions and 112 deletions

View File

@ -1339,6 +1339,9 @@ export const PROPERTY_PANE_EMPTY_SEARCH_RESULT_MESSAGE =
export const PROPERTY_SEARCH_INPUT_PLACEHOLDER =
"Search for controls, labels etc";
export const EXPLORER_BETA_ENTITY = () => "BETA";
export const BINDING_WIDGET_WALKTHROUGH_TITLE = () => "Widget properties";
export const BINDING_WIDGET_WALKTHROUGH_DESC = () =>
`Weve set the table data property for you. You can change it at anytime. The properties pane is a central hub for configuring widgets, allowing you to easily modify settings.`;
// API Pane
export const API_PANE_NO_BODY = () => "This request does not have a body";

View File

@ -47,11 +47,14 @@ import textWidgetIconSvg from "../../../widgets/TextWidget/icon.svg";
import listWidgetIconSvg from "../../../widgets/ListWidget/icon.svg";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import {
getFeatureFlagShownStatus,
getFeatureWalkthroughShown,
isUserSignedUpFlagSet,
setFeatureFlagShownStatus,
setFeatureWalkthroughShown,
} from "utils/storage";
import { getCurrentUser } from "selectors/usersSelectors";
import localStorage from "utils/localStorage";
import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
const BINDING_GUIDE_GIF = `${ASSETS_CDN_URL}/binding.gif`;
@ -362,14 +365,15 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
queryId?: string;
}>();
const closeWalkthrough = async () => {
if (isWalkthroughOpened) {
popFeature && popFeature();
await setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_binding_enabled, true);
}
const closeWalkthrough = () => {
popFeature && popFeature("BINDING_WIDGET");
setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_binding_enabled,
true,
);
};
const addWidget = (
const addWidget = async (
suggestedWidget: SuggestedWidget,
widgetInfo: WidgetBindingInfo,
) => {
@ -388,16 +392,25 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
AnalyticsUtil.logEvent("SUGGESTED_WIDGET_CLICK", {
widget: suggestedWidget.type,
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
FEATURE_WALKTHROUGH_KEYS.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForQueryBinding,
isWalkthroughOpened,
});
closeWalkthrough();
const showStatus = await getFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.binding_widget,
);
// To enable setting the widget id for showing walkthrough once the widget is created in WidgetOperationSagas.tsx -> addSuggestedWidget function
if (!showStatus && isEnabledForQueryBinding) {
(payload.props as any).setWidgetIdForWalkthrough = "true";
}
if (isWalkthroughOpened) {
closeWalkthrough();
}
dispatch(addSuggestedWidget(payload));
};
const handleBindData = (widgetId: string) => {
const handleBindData = async (widgetId: string) => {
dispatch(
bindDataOnCanvas({
queryId: (params.apiId || params.queryId) as string,
@ -406,12 +419,24 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
}),
);
closeWalkthrough();
if (isEnabledForQueryBinding) {
const value = await getFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.binding_widget,
);
if (!value) {
localStorage.setItem(WIDGET_ID_SHOW_WALKTHROUGH, widgetId);
}
}
dispatch(
bindDataToWidget({
widgetId: widgetId,
}),
);
if (isWalkthroughOpened) {
closeWalkthrough();
}
};
const isTableWidgetPresentOnCanvas = () => {
@ -443,8 +468,8 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
const isWidgetsPresentOnCanvas = Object.keys(canvasWidgets).length > 0;
const checkAndShowWalkthrough = async () => {
const isFeatureWalkthroughShown = await getFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_binding_enabled,
const isFeatureWalkthroughShown = await getFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_binding_enabled,
);
const isNewUser = user && (await isUserSignedUpFlagSet(user.email));
@ -455,14 +480,8 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
pushFeature({
targetId: BINDING_SECTION_ID,
onDismiss: async () => {
AnalyticsUtil.logEvent("WALKTHROUGH_DISMISSED", {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]:
isEnabledForQueryBinding,
});
await setFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_binding_enabled,
await setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_binding_enabled,
true,
);
},
@ -479,7 +498,7 @@ function SuggestedWidgets(props: SuggestedWidgetProps) {
},
eventParams: {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
FEATURE_WALKTHROUGH_KEYS.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForQueryBinding,
},
delay: 5000,

View File

@ -55,18 +55,18 @@ import { DatasourceComponentTypes } from "api/PluginApi";
import { fetchDatasourceStructure } from "actions/datasourceActions";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import {
getFeatureFlagShownStatus,
getFeatureWalkthroughShown,
isUserSignedUpFlagSet,
setFeatureFlagShownStatus,
setFeatureWalkthroughShown,
} from "utils/storage";
import { SCHEMA_SECTION_ID } from "entities/Action";
import { getCurrentUser } from "selectors/usersSelectors";
import { Tooltip } from "design-system";
import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
const SCHEMA_GUIDE_GIF = `${ASSETS_CDN_URL}/schema.gif`;
const SCHEMA_SECTION_ID = "t--api-right-pane-schema";
const SideBar = styled.div`
height: 100%;
width: 100%;
@ -378,8 +378,8 @@ function ActionSidebar({
}, []);
const checkAndShowWalkthrough = async () => {
const isFeatureWalkthroughShown = await getFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_schema_enabled,
const isFeatureWalkthroughShown = await getFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_schema_enabled,
);
const isNewUser = user && (await isUserSignedUpFlagSet(user.email));
@ -390,13 +390,8 @@ function ActionSidebar({
pushFeature({
targetId: SCHEMA_SECTION_ID,
onDismiss: async () => {
AnalyticsUtil.logEvent("WALKTHROUGH_DISMISSED", {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_schema_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema,
});
await setFeatureFlagShownStatus(
FEATURE_FLAG.ab_ds_schema_enabled,
await setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_schema_enabled,
true,
);
},
@ -416,7 +411,7 @@ function ActionSidebar({
},
eventParams: {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_schema_enabled,
FEATURE_WALKTHROUGH_KEYS.ab_ds_schema_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema,
},
delay: 5000,

View File

@ -1,7 +1,7 @@
import classNames from "classnames";
import * as Sentry from "@sentry/react";
import { useSelector } from "react-redux";
import React, { memo, useEffect, useMemo, useRef } from "react";
import React, { memo, useContext, useEffect, useMemo, useRef } from "react";
import PerformanceTracker, {
PerformanceTransactionName,
@ -21,6 +21,9 @@ import { getIsAppSettingsPaneOpen } from "selectors/appSettingsPaneSelectors";
import AppSettingsPane from "pages/Editor/AppSettingsPane";
import { APP_SETTINGS_PANE_WIDTH } from "constants/AppConstants";
import styled from "styled-components";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
export const PROPERTY_PANE_ID = "t--property-pane-sidebar";
const StyledResizer = styled.div<{ resizing: boolean }>`
${(props) =>
@ -54,6 +57,8 @@ export const PropertyPaneSidebar = memo((props: Props) => {
const selectedWidgetIds = useSelector(getSelectedWidgets);
const isDraggingOrResizing = useSelector(getIsDraggingOrResizing);
const isAppSettingsPaneOpen = useSelector(getIsAppSettingsPaneOpen);
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
//while dragging or resizing and
//the current selected WidgetId is not equal to previous widget id,
@ -109,6 +114,21 @@ export const PropertyPaneSidebar = memo((props: Props) => {
keepThemeWhileDragging,
]);
const closeWalkthrough = () => {
if (popFeature) {
popFeature("PROPERTY_PANE");
sidebarRef.current?.removeEventListener("click", closeWalkthrough);
}
};
useEffect(() => {
if (isWalkthroughOpened)
sidebarRef.current?.addEventListener("click", closeWalkthrough);
return () => {
sidebarRef.current?.removeEventListener("click", closeWalkthrough);
};
}, [isWalkthroughOpened]);
return (
<div className="relative">
{/* PROPERTY PANE */}
@ -119,6 +139,7 @@ export const PropertyPaneSidebar = memo((props: Props) => {
"relative ": !isPreviewMode,
"fixed translate-x-full right-0": isPreviewMode,
})}
id={PROPERTY_PANE_ID}
ref={sidebarRef}
>
{/* RESIZER */}

View File

@ -6,6 +6,7 @@ import { createPortal } from "react-dom";
import { hideIndicator } from "pages/Editor/GuidedTour/utils";
import { retryPromise } from "utils/AppsmithUtils";
import { useLocation } from "react-router-dom";
import AnalyticsUtil from "utils/AnalyticsUtil";
const WalkthroughRenderer = lazy(() => {
return retryPromise(
@ -36,8 +37,16 @@ export default function Walkthrough({ children }: any) {
updateActiveWalkthrough();
};
const popFeature = () => {
const popFeature = (triggeredFrom?: string) => {
hideIndicator();
const eventParams = activeWalkthrough?.eventParams || {};
if (triggeredFrom) {
eventParams.from = triggeredFrom;
}
AnalyticsUtil.logEvent("WALKTHROUGH_DISMISSED", eventParams);
if (activeWalkthrough && activeWalkthrough.onDismiss) {
activeWalkthrough.onDismiss();
}
setFeature((e) => {
e.shift();
return [...e];
@ -46,7 +55,7 @@ export default function Walkthrough({ children }: any) {
const updateActiveWalkthrough = () => {
if (feature.length > 0) {
const highlightArea = document.querySelector(`#${feature[0].targetId}`);
const highlightArea = document.getElementById(feature[0].targetId);
setActiveWalkthrough(null);
if (highlightArea) {
setTimeout(() => {

View File

@ -42,11 +42,13 @@ export type FeatureParams = {
eventParams?: Record<string, any>;
// Walkthrough delay in ms
delay?: number;
// Multiple Highlights -> multiple ids for highlighter, if not present considers targetId as the only highlighting div.
multipleHighlights?: string[];
};
type WalkthroughContextType = {
pushFeature: (feature: FeatureParams) => void;
popFeature: () => void;
popFeature: (triggeredFrom?: string) => void;
feature: FeatureParams[];
isOpened: boolean;
};

View File

@ -82,7 +82,7 @@ type RefRectParams = {
};
/*
* Clip Path Polygon :
* Clip Path Polygon for single target with bounding rect :
* 1) 0 0 ----> (body start) (body start)
* 2) 0 ${boundingRect.bh} ----> (body start) (body end)
* 3) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
@ -108,6 +108,9 @@ type RefRectParams = {
*
*
* 2 3,8 9
*
* Repeat steps 3 to 8 for each element if there are multiple highlighting elements.
*
*/
/**
@ -115,43 +118,62 @@ type RefRectParams = {
* @param targetId Id for the target container to show highlighting around it
*/
type BoundingRectTargets = Record<string, RefRectParams>;
const WalkthroughRenderer = ({
details,
offset,
onDismiss,
targetId,
eventParams = {},
multipleHighlights,
}: FeatureParams) => {
const [boundingRect, setBoundingRect] = useState<RefRectParams | null>(null);
const [boundingRects, setBoundingRects] =
useState<BoundingRectTargets | null>(null);
const { popFeature } = useContext(WalkthroughContext) || {};
const multipleHighlightsIds = multipleHighlights?.length
? multipleHighlights
: [targetId];
if (multipleHighlightsIds.indexOf(targetId) === -1)
multipleHighlightsIds.push(targetId);
const updateBoundingRect = () => {
const highlightArea = document.querySelector(`#${targetId}`);
if (highlightArea) {
const boundingRect = highlightArea.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
const offsetHighlightPad =
typeof offset?.highlightPad === "number"
? offset?.highlightPad
: PADDING_HIGHLIGHT;
setBoundingRect({
bw: bodyRect.width,
bh: bodyRect.height,
tw: boundingRect.width + 2 * offsetHighlightPad,
th: boundingRect.height + 2 * offsetHighlightPad,
tx: boundingRect.x - offsetHighlightPad,
ty: boundingRect.y - offsetHighlightPad,
});
showIndicator(`#${targetId}`, offset?.position, {
top: offset?.indicatorTop || 0,
left: offset?.indicatorLeft || 0,
zIndex: Z_INDEX + 1,
const mainTarget = document.getElementById(targetId);
if (mainTarget) {
const data: BoundingRectTargets = {};
multipleHighlightsIds.forEach((id) => {
const highlightArea = document.getElementById(id);
if (highlightArea) {
const boundingRect = highlightArea.getBoundingClientRect();
const bodyRect = document.body.getBoundingClientRect();
const offsetHighlightPad =
typeof offset?.highlightPad === "number"
? offset?.highlightPad
: PADDING_HIGHLIGHT;
data[id] = {
bw: bodyRect.width,
bh: bodyRect.height,
tw: boundingRect.width + 2 * offsetHighlightPad,
th: boundingRect.height + 2 * offsetHighlightPad,
tx: boundingRect.x - offsetHighlightPad,
ty: boundingRect.y - offsetHighlightPad,
};
}
});
if (Object.keys(data).length > 0) {
setBoundingRects(data);
showIndicator(`#${targetId}`, offset?.position, {
top: offset?.indicatorTop || 0,
left: offset?.indicatorLeft || 0,
zIndex: Z_INDEX + 1,
});
}
}
};
useEffect(() => {
updateBoundingRect();
const highlightArea = document.querySelector(`#${targetId}`);
const highlightArea = document.getElementById(targetId);
window.addEventListener("resize", updateBoundingRect);
const resizeObserver = new ResizeObserver(updateBoundingRect);
if (highlightArea) {
@ -165,37 +187,42 @@ const WalkthroughRenderer = ({
}, [targetId]);
const onDismissWalkthrough = () => {
onDismiss && onDismiss();
popFeature && popFeature();
popFeature && popFeature("WALKTHROUGH_CROSS_ICON");
};
if (!boundingRect) return null;
if (!boundingRects || Object.keys(boundingRects).length === 0) return null;
const targetBounds = boundingRects[targetId];
if (!targetBounds) return null;
return (
<WalkthroughWrapper className="t--walkthrough-overlay">
<SvgWrapper
height={boundingRect.bh}
width={boundingRect.bw}
height={targetBounds.bh}
width={targetBounds.bw}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<clipPath id={CLIPID}>
<polygon
// See the comments above the component declaration to understand the below points assignment.
points={`
0 0,
0 ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${
boundingRect.ty + boundingRect.th
},
${boundingRect.tx} ${boundingRect.ty + boundingRect.th},
${boundingRect.tx} ${boundingRect.bh},
${boundingRect.bw} ${boundingRect.bh},
${boundingRect.bw} 0
`}
points={`0 0,
0 ${targetBounds.bh},
${multipleHighlightsIds.reduce((acc, id) => {
const boundingRect = boundingRects[id];
acc = `${acc} ${boundingRect.tx} ${boundingRect.bh},
${boundingRect.tx} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${boundingRect.ty},
${boundingRect.tx + boundingRect.tw} ${
boundingRect.ty + boundingRect.th
},
${boundingRect.tx} ${boundingRect.ty + boundingRect.th},
${boundingRect.tx} ${boundingRect.bh},`;
return acc;
}, "")}
${targetBounds.bw} ${targetBounds.bh},
${targetBounds.bw} 0
`}
/>
</clipPath>
</defs>
@ -203,9 +230,9 @@ const WalkthroughRenderer = ({
style={{
clipPath: 'url("#' + CLIPID + '")',
fill: "currentcolor",
height: boundingRect.bh,
height: targetBounds.bh,
pointerEvents: "auto",
width: boundingRect.bw,
width: targetBounds.bw,
}}
/>
</SvgWrapper>

View File

@ -0,0 +1,7 @@
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
export const FEATURE_WALKTHROUGH_KEYS = {
[FEATURE_FLAG.ab_ds_binding_enabled]: FEATURE_FLAG.ab_ds_binding_enabled,
[FEATURE_FLAG.ab_ds_schema_enabled]: FEATURE_FLAG.ab_ds_schema_enabled,
binding_widget: "binding_widget",
};

View File

@ -252,3 +252,6 @@ export const SUGGESTED_WIDGETS_ORDER: Record<WidgetType, number> = {
SELECT_WIDGET: 5,
LIST_WIDGET_V2: 6,
};
// Constant key to show walkthrough for a widget -> stores widget id
export const WIDGET_ID_SHOW_WALKTHROUGH = "WIDGET_ID_SHOW_WALKTHROUGH";

View File

@ -239,3 +239,5 @@ export function getGraphQLPlugin(plugins: Plugin[]): Plugin | undefined {
export function isGraphqlPlugin(plugin: Plugin | undefined) {
return plugin?.packageName === PluginPackageName.GRAPHQL;
}
export const SCHEMA_SECTION_ID = "t--api-right-pane-schema";

View File

@ -1,4 +1,4 @@
import React, { useState, useContext } from "react";
import React, { useState } from "react";
import Entity, { EntityClassNames } from "../Entity";
import { datasourceTableIcon } from "../ExplorerIcons";
import QueryTemplates from "./QueryTemplates";
@ -17,9 +17,6 @@ import styled from "styled-components";
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
import AnalyticsUtil from "utils/AnalyticsUtil";
import type { Plugin } from "api/PluginApi";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { setFeatureFlagShownStatus } from "utils/storage";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
type DatasourceStructureProps = {
dbStructure: DatasourceTable;
@ -42,9 +39,6 @@ export function DatasourceStructure(props: DatasourceStructureProps) {
const [active, setActive] = useState(false);
useCloseMenuOnScroll(SIDEBAR_ID, active, () => setActive(false));
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
const datasource = useSelector((state: AppState) =>
getDatasource(state, props.datasourceId),
);
@ -72,15 +66,6 @@ export function DatasourceStructure(props: DatasourceStructureProps) {
});
canCreateDatasourceActions && setActive(!active);
dbStructure.templates.length === 0 &&
isWalkthroughOpened &&
closeWalkthrough();
};
const closeWalkthrough = () => {
popFeature && popFeature();
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
};
const lightningMenu =

View File

@ -9,6 +9,7 @@ import type {
DatasourceTable,
} from "entities/Datasource";
import type { ReactElement } from "react";
import { useContext } from "react";
import React, { memo, useEffect, useMemo, useState } from "react";
import EntityPlaceholder from "../Entity/Placeholder";
import DatasourceStructure from "./DatasourceStructure";
@ -21,6 +22,10 @@ import DatasourceStructureLoadingContainer from "./DatasourceStructureLoadingCon
import DatasourceStructureNotFound from "./DatasourceStructureNotFound";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { PluginName } from "entities/Action";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { setFeatureWalkthroughShown } from "utils/storage";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
import { SCHEMA_SECTION_ID } from "entities/Action";
type Props = {
datasourceId: string;
@ -71,6 +76,35 @@ const Container = (props: Props) => {
>(props.datasourceStructure);
const [hasSearchedOccured, setHasSearchedOccured] = useState(false);
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
const attachCloseWalkthrough =
props.context !== DatasourceStructureContext.EXPLORER &&
isWalkthroughOpened &&
!isLoading &&
!props.datasourceStructure?.tables?.length;
const closeWalkthrough = () => {
popFeature && popFeature("DATASOURCE_SCHEMA_CONTAINER");
setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_schema_enabled,
true,
);
};
useEffect(() => {
const schemaContainer = document.querySelector(`#${SCHEMA_SECTION_ID}`);
if (schemaContainer && attachCloseWalkthrough) {
schemaContainer.addEventListener("click", closeWalkthrough);
}
return () => {
if (schemaContainer && attachCloseWalkthrough) {
schemaContainer.removeEventListener("click", closeWalkthrough);
}
};
}, [attachCloseWalkthrough]);
useEffect(() => {
if (datasourceStructure !== props.datasourceStructure) {
setDatasourceStructure(props.datasourceStructure);

View File

@ -21,14 +21,14 @@ import { MenuItem } from "design-system";
import type { Plugin } from "api/PluginApi";
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { setFeatureFlagShownStatus } from "utils/storage";
import { setFeatureWalkthroughShown } from "utils/storage";
import styled from "styled-components";
import { change, getFormValues } from "redux-form";
import { QUERY_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
import { diff } from "deep-diff";
import { UndoRedoToastContext, showUndoRedoToast } from "utils/replayHelpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
type QueryTemplatesProps = {
templates: QueryTemplate[];
@ -110,8 +110,11 @@ export function QueryTemplates(props: QueryTemplatesProps) {
);
if (isWalkthroughOpened) {
popFeature && popFeature();
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
popFeature && popFeature("SCHEMA_QUERY_CREATE");
setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_schema_enabled,
true,
);
}
history.push(
@ -170,8 +173,11 @@ export function QueryTemplates(props: QueryTemplatesProps) {
});
if (isWalkthroughOpened) {
popFeature && popFeature();
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
popFeature && popFeature("SCHEMA_QUERY_UPDATE");
setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.ab_ds_schema_enabled,
true,
);
}
showUndoRedoToast(

View File

@ -1,4 +1,5 @@
import type { ReactElement } from "react";
import { useContext } from "react";
import React, { useCallback, useEffect, useMemo, useRef } from "react";
import equal from "fast-deep-equal/es6";
import { useDispatch, useSelector } from "react-redux";
@ -12,6 +13,7 @@ import { deleteSelectedWidget, copyWidget } from "actions/widgetActions";
import ConnectDataCTA, { actionsExist } from "./ConnectDataCTA";
import PropertyPaneConnections from "./PropertyPaneConnections";
import type { WidgetType } from "constants/WidgetConstants";
import { WIDGET_ID_SHOW_WALKTHROUGH } from "constants/WidgetConstants";
import type { InteractionAnalyticsEventDetail } from "utils/AppsmithUtils";
import { INTERACTION_ANALYTICS_EVENT } from "utils/AppsmithUtils";
import { emitInteractionAnalyticsEvent } from "utils/AppsmithUtils";
@ -23,6 +25,18 @@ import { PropertyPaneTab } from "./PropertyPaneTab";
import { useSearchText } from "./helpers";
import { PropertyPaneSearchInput } from "./PropertyPaneSearchInput";
import { sendPropertyPaneSearchAnalytics } from "./propertyPaneSearch";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
import { AB_TESTING_EVENT_KEYS } from "@appsmith/entities/FeatureFlag";
import localStorage from "utils/localStorage";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
import { PROPERTY_PANE_ID } from "components/editorComponents/PropertyPaneSidebar";
import { setFeatureWalkthroughShown } from "utils/storage";
import {
BINDING_WIDGET_WALKTHROUGH_DESC,
BINDING_WIDGET_WALKTHROUGH_TITLE,
createMessage,
} from "@appsmith/constants/messages";
import { getWidgets } from "sagas/selectors";
// TODO(abhinav): The widget should add a flag in their configuration if they donot subscribe to data
// Widgets where we do not want to show the CTA
@ -65,6 +79,52 @@ function PropertyPaneView(
return true;
}, [widgetProperties?.type, excludeList]);
const { searchText, setSearchText } = useSearchText("");
const { pushFeature } = useContext(WalkthroughContext) || {};
const widgets = useSelector(getWidgets);
const showWalkthroughIfWidgetIdSet = async () => {
const widgetId: string | null = await localStorage.getItem(
WIDGET_ID_SHOW_WALKTHROUGH,
);
// Adding table condition as connecting to select, chart widgets is currently not working as expected
// When we fix those, we can remove this table condtion
const isTableWidget = !!widgetId
? widgets[widgetId]?.type === "TABLE_WIDGET_V2"
: false;
if (widgetId && pushFeature && isTableWidget) {
pushFeature({
targetId: PROPERTY_PANE_ID,
onDismiss: async () => {
await localStorage.removeItem(WIDGET_ID_SHOW_WALKTHROUGH);
await setFeatureWalkthroughShown(
FEATURE_WALKTHROUGH_KEYS.binding_widget,
true,
);
},
details: {
title: createMessage(BINDING_WIDGET_WALKTHROUGH_TITLE),
description: createMessage(BINDING_WIDGET_WALKTHROUGH_DESC),
},
offset: {
position: "left",
left: -40,
top: 250,
highlightPad: 2,
indicatorLeft: -3,
indicatorTop: 230,
},
eventParams: {
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_WALKTHROUGH_KEYS.binding_widget,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: true,
},
multipleHighlights: [widgetId, PROPERTY_PANE_ID],
delay: 5000,
});
}
};
const handleKbdEvent = (e: Event) => {
const event = e as CustomEvent<InteractionAnalyticsEventDetail>;
@ -81,6 +141,7 @@ function PropertyPaneView(
INTERACTION_ANALYTICS_EVENT,
handleKbdEvent,
);
showWalkthroughIfWidgetIdSet();
return () => {
containerRef.current?.removeEventListener(
INTERACTION_ANALYTICS_EVENT,

View File

@ -26,6 +26,7 @@ import {
FEATURE_FLAG,
} from "@appsmith/entities/FeatureFlag";
import { selectFeatureFlagCheck } from "@appsmith/selectors/featureFlagsSelectors";
import { FEATURE_WALKTHROUGH_KEYS } from "constants/WalkthroughConstants";
import type { PropertyUpdates } from "widgets/constants";
export function* bindDataToWidgetSaga(
@ -78,7 +79,7 @@ export function* bindDataToWidgetSaga(
propertyPath: updates?.map((update) => update.propertyPath).toString(),
propertyValue: updates?.map((update) => update.propertyPath).toString(),
[AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
FEATURE_FLAG.ab_ds_binding_enabled,
FEATURE_WALKTHROUGH_KEYS.ab_ds_binding_enabled,
[AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isDSBindingEnabled,
});
} else {

View File

@ -13,6 +13,7 @@ import {
GridDefaults,
MAIN_CONTAINER_WIDGET_ID,
RenderModes,
WIDGET_ID_SHOW_WALKTHROUGH,
} from "constants/WidgetConstants";
import log from "loglevel";
import type { WidgetResize } from "actions/pageActions";
@ -181,6 +182,7 @@ import {
FlexLayerAlignment,
LayoutDirection,
} from "utils/autoLayout/constants";
import localStorage from "utils/localStorage";
export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
try {
@ -2017,7 +2019,11 @@ function* cutWidgetSaga() {
}
function* addSuggestedWidget(action: ReduxAction<Partial<WidgetProps>>) {
const isSetWidgetIdForWalkthrough = !!(
action.payload.props.setWidgetIdForWalkthrough === "true"
);
const widgetConfig = action.payload;
delete widgetConfig.props?.setWidgetIdForWalkthrough;
if (!widgetConfig.type) return;
@ -2079,6 +2085,10 @@ function* addSuggestedWidget(action: ReduxAction<Partial<WidgetProps>>) {
yield take(ReduxActionTypes.UPDATE_LAYOUT);
if (isSetWidgetIdForWalkthrough) {
localStorage.setItem(WIDGET_ID_SHOW_WALKTHROUGH, newWidget.newWidgetId);
}
yield put(
selectWidgetInitAction(SelectionRequestType.One, [newWidget.newWidgetId]),
);

View File

@ -457,7 +457,7 @@ export const getAIPromptTriggered = async () => {
return 0;
}
};
export const setFeatureFlagShownStatus = async (key: string, value: any) => {
export const setFeatureWalkthroughShown = async (key: string, value: any) => {
try {
let flagsJSON: Record<string, any> | null = await store.getItem(
STORAGE_KEYS.FEATURE_WALKTHROUGH,
@ -477,7 +477,7 @@ export const setFeatureFlagShownStatus = async (key: string, value: any) => {
}
};
export const getFeatureFlagShownStatus = async (key: string) => {
export const getFeatureWalkthroughShown = async (key: string) => {
try {
const flagsJSON: Record<string, any> | null = await store.getItem(
STORAGE_KEYS.FEATURE_WALKTHROUGH,

View File

@ -1,3 +1,4 @@
import { useContext } from "react";
import type {
MouseEventHandler,
PropsWithChildren,
@ -17,6 +18,7 @@ import { useSelector } from "react-redux";
import { getCurrentAppPositioningType } from "selectors/editorSelectors";
import { AppPositioningTypes } from "reducers/entityReducers/pageListReducer";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
const StyledContainerComponent = styled.div<
Omit<ContainerWrapperProps, "widgetId">
@ -59,6 +61,8 @@ function ContainerComponentWrapper(
) {
const containerRef: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
const appPositioningType = useSelector(getCurrentAppPositioningType);
const { isOpened: isWalkthroughOpened, popFeature } =
useContext(WalkthroughContext) || {};
useEffect(() => {
if (!props.shouldScrollContents) {
@ -117,6 +121,17 @@ function ContainerComponentWrapper(
[props.onClickCapture],
);
const closeWalkthrough = () => {
if (isWalkthroughOpened && popFeature) {
popFeature("WIDGET_CONTAINER");
}
};
const onClickHandler = (event: any) => {
closeWalkthrough();
if (props.onClick) props.onClick(event);
};
return (
<StyledContainerComponent
// Before you remove: generateClassName is used for bounding the resizables within this canvas
@ -133,7 +148,7 @@ function ContainerComponentWrapper(
}`}
data-widgetId={props.widgetId}
dropDisabled={props.dropDisabled}
onClick={props.onClick}
onClick={onClickHandler}
onClickCapture={props.onClickCapture}
onMouseOver={onMouseOver}
ref={containerRef}