From e3e75acc329dc16a86ef90dbcf68a2b84c38691d Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Thu, 27 Jul 2023 18:30:23 +0530 Subject: [PATCH] feat: multiple highlight area in walkthrough, corrected walkthrough events for each feature (#25520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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”> --- app/client/src/ce/constants/messages.ts | 3 + .../ActionRightPane/SuggestedWidgets.tsx | 65 ++++++---- .../ActionRightPane/index.tsx | 23 ++-- .../editorComponents/PropertyPaneSidebar.tsx | 23 +++- .../components/featureWalkthrough/index.tsx | 13 +- .../featureWalkthrough/walkthroughContext.tsx | 4 +- .../walkthroughRenderer.tsx | 117 +++++++++++------- .../src/constants/WalkthroughConstants.ts | 7 ++ app/client/src/constants/WidgetConstants.tsx | 3 + app/client/src/entities/Action/index.ts | 2 + .../Datasources/DatasourceStructure.tsx | 17 +-- .../DatasourceStructureContainer.tsx | 34 +++++ .../Explorer/Datasources/QueryTemplates.tsx | 18 ++- .../Editor/PropertyPane/PropertyPaneView.tsx | 61 +++++++++ app/client/src/sagas/SnipingModeSagas.ts | 3 +- app/client/src/sagas/WidgetOperationSagas.tsx | 10 ++ app/client/src/utils/storage.ts | 4 +- .../ContainerWidget/component/index.tsx | 17 ++- 18 files changed, 312 insertions(+), 112 deletions(-) create mode 100644 app/client/src/constants/WalkthroughConstants.ts diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index 99156677d5..80017d7e43 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -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 = () => + `We’ve 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"; diff --git a/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx b/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx index 5343a217ec..1c7ba41894 100644 --- a/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx +++ b/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx @@ -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, diff --git a/app/client/src/components/editorComponents/ActionRightPane/index.tsx b/app/client/src/components/editorComponents/ActionRightPane/index.tsx index 7132626b00..acadfb4ea6 100644 --- a/app/client/src/components/editorComponents/ActionRightPane/index.tsx +++ b/app/client/src/components/editorComponents/ActionRightPane/index.tsx @@ -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, diff --git a/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx b/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx index e4d086810b..2f5bc16434 100644 --- a/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx +++ b/app/client/src/components/editorComponents/PropertyPaneSidebar.tsx @@ -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 (
{/* 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 */} diff --git a/app/client/src/components/featureWalkthrough/index.tsx b/app/client/src/components/featureWalkthrough/index.tsx index b3f81f92ca..71f26aef4b 100644 --- a/app/client/src/components/featureWalkthrough/index.tsx +++ b/app/client/src/components/featureWalkthrough/index.tsx @@ -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(() => { diff --git a/app/client/src/components/featureWalkthrough/walkthroughContext.tsx b/app/client/src/components/featureWalkthrough/walkthroughContext.tsx index 28ec14ac76..2035132006 100644 --- a/app/client/src/components/featureWalkthrough/walkthroughContext.tsx +++ b/app/client/src/components/featureWalkthrough/walkthroughContext.tsx @@ -42,11 +42,13 @@ export type FeatureParams = { eventParams?: Record; // 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; }; diff --git a/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx b/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx index 5cdd21b90e..bc5360c049 100644 --- a/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx +++ b/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx @@ -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; + const WalkthroughRenderer = ({ details, offset, - onDismiss, targetId, eventParams = {}, + multipleHighlights, }: FeatureParams) => { - const [boundingRect, setBoundingRect] = useState(null); + const [boundingRects, setBoundingRects] = + useState(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 ( { + 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 + `} /> @@ -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, }} /> diff --git a/app/client/src/constants/WalkthroughConstants.ts b/app/client/src/constants/WalkthroughConstants.ts new file mode 100644 index 0000000000..1b0fbec3ec --- /dev/null +++ b/app/client/src/constants/WalkthroughConstants.ts @@ -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", +}; diff --git a/app/client/src/constants/WidgetConstants.tsx b/app/client/src/constants/WidgetConstants.tsx index c44f3cb0b0..cc7326294a 100644 --- a/app/client/src/constants/WidgetConstants.tsx +++ b/app/client/src/constants/WidgetConstants.tsx @@ -252,3 +252,6 @@ export const SUGGESTED_WIDGETS_ORDER: Record = { 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"; diff --git a/app/client/src/entities/Action/index.ts b/app/client/src/entities/Action/index.ts index 18ea8f72bb..f27eed1f2f 100644 --- a/app/client/src/entities/Action/index.ts +++ b/app/client/src/entities/Action/index.ts @@ -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"; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx index 13a5123b12..e1c2c6a446 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructure.tsx @@ -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 = diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx index a31d194317..411881bd29 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx @@ -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); diff --git a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx index 1450313194..a8d742ec40 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx @@ -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( diff --git a/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx b/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx index 0e2f8c01e0..d5b0c92dff 100644 --- a/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx +++ b/app/client/src/pages/Editor/PropertyPane/PropertyPaneView.tsx @@ -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; @@ -81,6 +141,7 @@ function PropertyPaneView( INTERACTION_ANALYTICS_EVENT, handleKbdEvent, ); + showWalkthroughIfWidgetIdSet(); return () => { containerRef.current?.removeEventListener( INTERACTION_ANALYTICS_EVENT, diff --git a/app/client/src/sagas/SnipingModeSagas.ts b/app/client/src/sagas/SnipingModeSagas.ts index 1a54201c2a..364065dcd0 100644 --- a/app/client/src/sagas/SnipingModeSagas.ts +++ b/app/client/src/sagas/SnipingModeSagas.ts @@ -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 { diff --git a/app/client/src/sagas/WidgetOperationSagas.tsx b/app/client/src/sagas/WidgetOperationSagas.tsx index 531a47fa80..9089d58bf9 100644 --- a/app/client/src/sagas/WidgetOperationSagas.tsx +++ b/app/client/src/sagas/WidgetOperationSagas.tsx @@ -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) { try { @@ -2017,7 +2019,11 @@ function* cutWidgetSaga() { } function* addSuggestedWidget(action: ReduxAction>) { + 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>) { yield take(ReduxActionTypes.UPDATE_LAYOUT); + if (isSetWidgetIdForWalkthrough) { + localStorage.setItem(WIDGET_ID_SHOW_WALKTHROUGH, newWidget.newWidgetId); + } + yield put( selectWidgetInitAction(SelectionRequestType.One, [newWidget.newWidgetId]), ); diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index 1b8220688b..f007ecd425 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -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 | 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 | null = await store.getItem( STORAGE_KEYS.FEATURE_WALKTHROUGH, diff --git a/app/client/src/widgets/ContainerWidget/component/index.tsx b/app/client/src/widgets/ContainerWidget/component/index.tsx index a3365d2995..b162879d0f 100644 --- a/app/client/src/widgets/ContainerWidget/component/index.tsx +++ b/app/client/src/widgets/ContainerWidget/component/index.tsx @@ -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 @@ -59,6 +61,8 @@ function ContainerComponentWrapper( ) { const containerRef: RefObject = useRef(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 (