diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/ApiBugs_Spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/ApiBugs_Spec.ts index 327291e2c0..c7ab7ea952 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/BugTests/ApiBugs_Spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/ApiBugs_Spec.ts @@ -13,8 +13,18 @@ import { ERROR_ACTION_EXECUTE_FAIL, createMessage, } from "../../../../support/Objects/CommonErrorMessages"; +import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags"; describe("API Bugs", function () { + before(() => { + featureFlagIntercept( + { + ab_ds_binding_enabled: false, + }, + false, + ); + agHelper.RefreshPage(); + }); it("1. Bug 14037: User gets an error even when table widget is added from the API page successfully", function () { apiPage.CreateAndFillApi(tedTestConfig.mockApiUrl, "Api1"); apiPage.RunAPI(); diff --git a/app/client/cypress/e2e/Regression/ClientSide/BugTests/DatasourceSchema_spec.ts b/app/client/cypress/e2e/Regression/ClientSide/BugTests/DatasourceSchema_spec.ts index 5368651420..5c0399bb35 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/BugTests/DatasourceSchema_spec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/BugTests/DatasourceSchema_spec.ts @@ -1,3 +1,4 @@ +import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags"; import { ObjectsRegistry } from "../../../../support/Objects/Registry"; const agHelper = ObjectsRegistry.AggregateHelper, @@ -40,4 +41,24 @@ describe("Datasource form related tests", function () { dataSources.DeleteQuery("Query1"); dataSources.DeleteDatasouceFromWinthinDS(dataSourceName); }); + + it("3. Verify if schema (table and column) exist in query editor and searching works", () => { + featureFlagIntercept( + { + ab_ds_schema_enabled: true, + }, + false, + ); + agHelper.RefreshPage(); + dataSources.CreateMockDB("Users"); + dataSources.CreateQueryAfterDSSaved(); + dataSources.VerifyTableSchemaOnQueryEditor("public.users"); + ee.ExpandCollapseEntity("public.users"); + dataSources.VerifyColumnSchemaOnQueryEditor("id"); + dataSources.FilterAndVerifyDatasourceSchemaBySearch( + "gender", + true, + "column", + ); + }); }); diff --git a/app/client/cypress/e2e/Regression/ClientSide/MobileResponsiveTests/SuggestedWidgets_spec.js b/app/client/cypress/e2e/Regression/ClientSide/MobileResponsiveTests/SuggestedWidgets_spec.js index a55eea514e..d485b4aaad 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/MobileResponsiveTests/SuggestedWidgets_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/MobileResponsiveTests/SuggestedWidgets_spec.js @@ -2,12 +2,21 @@ import { autoLayout, dataSources, table, + agHelper, } from "../../../../support/Objects/ObjectsCore"; import { Widgets } from "../../../../support/Pages/DataSources"; +import { featureFlagIntercept } from "../../../../support/Objects/FeatureFlags"; describe("Check Suggested Widgets Feature in auto-layout", function () { before(() => { autoLayout.ConvertToAutoLayoutAndVerify(false); + featureFlagIntercept( + { + ab_ds_binding_enabled: true, + }, + false, + ); + agHelper.RefreshPage(); }); it("1. Suggested widget", () => { diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Button/Button_onClickAction_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Button/Button_onClickAction_spec.js index c18344cbe7..0d92b2f7cb 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Button/Button_onClickAction_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Button/Button_onClickAction_spec.js @@ -38,7 +38,6 @@ describe("Button Widget Functionality", function () { ); cy.SaveAndRunAPI(); - // Going to HomePage where the button widget is located and opening it's property pane. _.entityExplorer.ExpandCollapseEntity("Widgets"); _.entityExplorer.ExpandCollapseEntity("Container3"); _.entityExplorer.SelectEntityByName("Button1"); @@ -69,8 +68,9 @@ describe("Button Widget Functionality", function () { // Creating a mock query // cy.CreateMockQuery("Query1"); _.dataSources.CreateDataSource("Postgres"); - _.entityExplorer.ActionTemplateMenuByEntityName("public.film", "SELECT"); - // Going to HomePage where the button widget is located and opeing it's property pane. + _.dataSources.CreateQueryAfterDSSaved( + `SELECT * FROM public."film" LIMIT 10;`, + ); _.entityExplorer.ExpandCollapseEntity("Container3"); _.entityExplorer.SelectEntityByName("Button1"); diff --git a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_2_spec.js b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_2_spec.js index 1b80196a9f..a61205d92e 100644 --- a/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_2_spec.js +++ b/app/client/cypress/e2e/Regression/ServerSide/QueryPane/S3_2_spec.js @@ -222,12 +222,6 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications", _.entityExplorer.SelectEntityByName("Table1", "Widgets"); _.agHelper.GetNClick(_.propPane._deleteWidget); - _.entityExplorer.SelectEntityByName($queryName, "Queries/JS"); - cy.xpath(queryLocators.suggestedWidgetText).click().wait(1000); - cy.get(commonlocators.textWidget).validateWidgetExists(); - _.entityExplorer.SelectEntityByName("Text1", "Widgets"); - _.agHelper.GetNClick(_.propPane._deleteWidget); - _.entityExplorer.SelectEntityByName($queryName, "Queries/JS"); cy.deleteQueryUsingContext(); //exeute actions & 200 response is verified in this method }); diff --git a/app/client/cypress/e2e/Sanity/Datasources/MsSQL_Basic_Spec.ts b/app/client/cypress/e2e/Sanity/Datasources/MsSQL_Basic_Spec.ts index 895d1bad4e..a6d10560f4 100644 --- a/app/client/cypress/e2e/Sanity/Datasources/MsSQL_Basic_Spec.ts +++ b/app/client/cypress/e2e/Sanity/Datasources/MsSQL_Basic_Spec.ts @@ -1,3 +1,4 @@ +import { featureFlagIntercept } from "../../../support/Objects/FeatureFlags"; import { agHelper, assertHelper, @@ -87,6 +88,13 @@ describe("Validate MsSQL connection & basic querying with UI flows", () => { dataSources.RunQuery(); }); //agHelper.ActionContextMenuWithInPane("Delete"); Since next case can continue in same template + featureFlagIntercept( + { + ab_ds_binding_enabled: false, + }, + false, + ); + agHelper.RefreshPage(); }); it("1. Validate simple queries - Show all existing tables, Describe table & verify query responses", () => { diff --git a/app/client/cypress/support/Pages/DataSources.ts b/app/client/cypress/support/Pages/DataSources.ts index 8661b8b33e..d5d413853c 100644 --- a/app/client/cypress/support/Pages/DataSources.ts +++ b/app/client/cypress/support/Pages/DataSources.ts @@ -231,6 +231,10 @@ export class DataSources { _bodyCodeMirror = "//div[contains(@class, 't--actionConfiguration.body')]"; private _reconnectModalDSToolTip = ".t--ds-list .t--ds-list-title"; private _reconnectModalDSToopTipIcon = ".t--ds-list .ads-v2-icon"; + private _datasourceTableSchemaInQueryEditor = + ".datasourceStructure-query-editor"; + private _datasourceColumnSchemaInQueryEditor = ".t--datasource-column"; + private _datasourceStructureSearchInput = ".datasourceStructure-search input"; public AssertDSEditViewMode(mode: "Edit" | "View") { if (mode == "Edit") this.agHelper.AssertElementAbsence(this._editButton); @@ -1198,6 +1202,35 @@ export class DataSources { }); } + public VerifyTableSchemaOnQueryEditor(schema: string) { + this.agHelper + .GetElement(this._datasourceTableSchemaInQueryEditor) + .contains(schema); + } + + public VerifyColumnSchemaOnQueryEditor(schema: string, index = 0) { + this.agHelper + .GetElement(this._datasourceColumnSchemaInQueryEditor) + .eq(index) + .contains(schema); + } + + public FilterAndVerifyDatasourceSchemaBySearch( + search: string, + verifySearch = false, + filterBy: "table" | "column" = "column", + ) { + this.agHelper.TypeText(this._datasourceStructureSearchInput, search); + + if (verifySearch) { + if (filterBy === "column") { + this.VerifyColumnSchemaOnQueryEditor(search); + } else { + this.VerifyTableSchemaOnQueryEditor(search); + } + } + } + public SaveDSFromDialog(save = true) { this.agHelper.GoBack(); this.agHelper.AssertElementVisible(this._datasourceModalDoNotSave); diff --git a/app/client/src/actions/datasourceActions.ts b/app/client/src/actions/datasourceActions.ts index beafccdd15..5607a12d73 100644 --- a/app/client/src/actions/datasourceActions.ts +++ b/app/client/src/actions/datasourceActions.ts @@ -14,6 +14,7 @@ import type { PluginType } from "entities/Action"; import type { executeDatasourceQueryRequest } from "api/DatasourcesApi"; import type { ResponseMeta } from "api/ApiResponses"; import { TEMP_DATASOURCE_ID } from "constants/Datasource"; +import type { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer"; export const createDatasourceFromForm = ( payload: CreateDatasourceConfig & Datasource, @@ -106,12 +107,17 @@ export const redirectAuthorizationCode = ( }; }; -export const fetchDatasourceStructure = (id: string, ignoreCache?: boolean) => { +export const fetchDatasourceStructure = ( + id: string, + ignoreCache?: boolean, + schemaFetchContext?: DatasourceStructureContext, +) => { return { type: ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT, payload: { id, ignoreCache, + schemaFetchContext, }, }; }; @@ -160,11 +166,15 @@ export const expandDatasourceEntity = (id: string) => { }; }; -export const refreshDatasourceStructure = (id: string) => { +export const refreshDatasourceStructure = ( + id: string, + schemaRefreshContext?: DatasourceStructureContext, +) => { return { type: ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_INIT, payload: { id, + schemaRefreshContext, }, }; }; diff --git a/app/client/src/ce/AppRouter.tsx b/app/client/src/ce/AppRouter.tsx index 4c08885045..879dba287b 100644 --- a/app/client/src/ce/AppRouter.tsx +++ b/app/client/src/ce/AppRouter.tsx @@ -62,6 +62,7 @@ import { import useBrandingTheme from "utils/hooks/useBrandingTheme"; import RouteChangeListener from "RouteChangeListener"; import { initCurrentPage } from "../actions/initActions"; +import Walkthrough from "components/featureWalkthrough"; export const SentryRoute = Sentry.withSentryRouting(Route); @@ -175,10 +176,10 @@ function AppRouter(props: { ) : ( - <> + - + )} diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index ec221cfe15..7cc4ef0cf2 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -647,6 +647,9 @@ export const BULK_WIDGET_REMOVED = (widgetName: string) => export const BULK_WIDGET_ADDED = (widgetName: string) => `${widgetName} widgets are added back`; +export const ACTION_CONFIGURATION_CHANGED = (name: string) => + `${name}'s configuration has changed`; + // Generate page from DB Messages export const UNSUPPORTED_PLUGIN_DIALOG_TITLE = () => @@ -691,10 +694,37 @@ export const ADD_NEW_WIDGET = () => "Add new widget"; export const SUGGESTED_WIDGETS = () => "Suggested widgets"; export const SUGGESTED_WIDGET_TOOLTIP = () => "Add to canvas"; export const WELCOME_TOUR_STICKY_BUTTON_TEXT = () => "Next mission"; +export const BINDING_SECTION_LABEL = () => "Bindings"; +export const ADD_NEW_WIDGET_SUB_HEADING = () => + "Select how you want to display data."; +export const CONNECT_EXISTING_WIDGET_LABEL = () => "Select a Widget"; +export const CONNECT_EXISTING_WIDGET_SUB_HEADING = () => + "Replace the data of an existing widget"; +export const NO_EXISTING_WIDGETS = () => "Display data in a new widget"; +export const BINDING_WALKTHROUGH_TITLE = () => "Display your data"; +export const BINDING_WALKTHROUGH_DESC = () => + "You can replace data of an existing widget of your page or you can select a new widget."; +export const BINDINGS_DISABLED_TOOLTIP = () => + "You can display data when you have a successful response to your query"; // Data Sources pane export const EMPTY_ACTIVE_DATA_SOURCES = () => "No active datasources found."; + +// Datasource structure + export const SCHEMA_NOT_AVAILABLE = () => "Schema not available"; +export const TABLE_OR_COLUMN_NOT_FOUND = () => "Table or column not found."; +export const DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT = () => + "Search for table or attribute"; +export const SCHEMA_LABEL = () => "Schema"; +export const STRUCTURE_NOT_FETCHED = () => + "We could not fetch the schema of the database."; +export const TEST_DATASOURCE_AND_FIX_ERRORS = () => + "Test the datasource and fix the errors."; +export const LOADING_SCHEMA = () => "Loading schema..."; +export const SCHEMA_WALKTHROUGH_TITLE = () => "Query data fast"; +export const SCHEMA_WALKTHROUGH_DESC = () => + "Select a template from a database table to quickly create your first query. "; // Git sync export const CONNECTED_TO_GIT = () => "Connected to Git"; diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index 5183ca780a..258f3c2e83 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -1,3 +1,4 @@ +// Please follow naming convention : https://www.notion.so/appsmith/Using-Feature-Flags-in-Appsmith-d362fe7acc7d4ef0aa12e1f5f9b83b5f?pvs=4#f6d4242e56284e84af25cadef71b7aeb to create feature flags. export const FEATURE_FLAG = { TEST_FLAG: "TEST_FLAG", release_datasource_environments_enabled: @@ -8,6 +9,8 @@ export const FEATURE_FLAG = { ask_ai_js: "ask_ai_js", APP_EMBED_VIEW_HIDE_SHARE_SETTINGS_VISIBILITY: "APP_EMBED_VIEW_HIDE_SHARE_SETTINGS_VISIBILITY", + ab_ds_schema_enabled: "ab_ds_schema_enabled", + ab_ds_binding_enabled: "ab_ds_binding_enabled", } as const; export type FeatureFlag = keyof typeof FEATURE_FLAG; @@ -22,4 +25,11 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = { ask_ai_js: false, ask_ai_sql: false, APP_EMBED_VIEW_HIDE_SHARE_SETTINGS_VISIBILITY: false, + ab_ds_schema_enabled: false, + ab_ds_binding_enabled: false, +}; + +export const AB_TESTING_EVENT_KEYS = { + abTestingFlagLabel: "abTestingFlagLabel", + abTestingFlagValue: "abTestingFlagValue", }; diff --git a/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx b/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx index 33d3e532ac..0e7264436a 100644 --- a/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx +++ b/app/client/src/components/editorComponents/ActionRightPane/SuggestedWidgets.tsx @@ -1,4 +1,4 @@ -import React, { memo } from "react"; +import React, { memo, useContext, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; import styled from "styled-components"; import { generateReactKey } from "utils/generators"; @@ -8,9 +8,16 @@ import { addSuggestedWidget } from "actions/widgetActions"; import AnalyticsUtil from "utils/AnalyticsUtil"; import { ADD_NEW_WIDGET, + ADD_NEW_WIDGET_SUB_HEADING, + BINDING_SECTION_LABEL, + CONNECT_EXISTING_WIDGET_LABEL, + CONNECT_EXISTING_WIDGET_SUB_HEADING, createMessage, + NO_EXISTING_WIDGETS, SUGGESTED_WIDGETS, SUGGESTED_WIDGET_TOOLTIP, + BINDING_WALKTHROUGH_TITLE, + BINDING_WALKTHROUGH_DESC, } from "@appsmith/constants/messages"; import type { SuggestedWidget } from "api/ActionAPI"; @@ -20,8 +27,39 @@ import { getNextWidgetName } from "sagas/WidgetOperationUtils"; import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; import { getAssetUrl } from "@appsmith/utils/airgapHelpers"; import { Tooltip } from "design-system"; +import type { TextKind } from "design-system"; +import { Text } from "design-system"; +import { + AB_TESTING_EVENT_KEYS, + FEATURE_FLAG, +} from "@appsmith/entities/FeatureFlag"; +import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors"; +import type { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsStructureReducer"; +import { useParams } from "react-router"; +import { getCurrentApplicationId } from "selectors/editorSelectors"; +import { bindDataOnCanvas } from "actions/pluginActionActions"; +import { bindDataToWidget } from "actions/propertyPaneActions"; +import tableWidgetIconSvg from "../../../widgets/TableWidgetV2/icon.svg"; +import selectWidgetIconSvg from "../../../widgets/SelectWidget/icon.svg"; +import chartWidgetIconSvg from "../../../widgets/ChartWidget/icon.svg"; +import inputWidgetIconSvg from "../../../widgets/InputWidgetV2/icon.svg"; +import textWidgetIconSvg from "../../../widgets/TextWidget/icon.svg"; +import listWidgetIconSvg from "../../../widgets/ListWidget/icon.svg"; +import WalkthroughContext from "components/featureWalkthrough/walkthroughContext"; +import { + getFeatureFlagShownStatus, + isUserSignedUpFlagSet, + setFeatureFlagShownStatus, +} from "utils/storage"; +import { getCurrentUser } from "selectors/usersSelectors"; + +const BINDING_GUIDE_GIF = `${ASSETS_CDN_URL}/binding.gif`; + +const BINDING_SECTION_ID = "t--api-right-pane-binding"; const WidgetList = styled.div` + height: 100%; + overflow: auto; ${getTypographyByKey("p1")} margin-left: ${(props) => props.theme.spaces[2] + 1}px; @@ -33,6 +71,8 @@ const WidgetList = styled.div` .image-wrapper { position: relative; margin-top: ${(props) => props.theme.spaces[1]}px; + display: flex; + flex-direction: column; } .widget:hover { @@ -42,6 +82,76 @@ const WidgetList = styled.div` .widget:not(:first-child) { margin-top: 24px; } + + &.spacing { + .widget:not(:first-child) { + margin-top: 16px; + } + } +`; + +const ExistingWidgetList = styled.div` + display: flex; + flex-wrap: wrap; + + .image-wrapper { + position: relative; + display: flex; + flex-direction: column; + width: 110px; + margin: 4px; + border: 1px solid var(--ads-v2-color-gray-300); + border-radius: var(--ads-v2-border-radius); + + &:hover { + border: 1px solid var(--ads-v2-color-gray-600); + } + } + + img { + height: 54px; + } + + .widget:hover { + cursor: pointer; + } +`; + +const ItemWrapper = styled.div` + display: flex; + align-items: center; + padding: 4px; + + .widget-name { + padding-left: 8px; + overflow: hidden; + text-overflow: ellipsis; + } + + img { + height: 16px; + width: 16px; + } +`; + +const SubSection = styled.div` + margin-bottom: ${(props) => props.theme.spaces[7]}px; + overflow-y: scroll; + height: 100%; +`; + +const HeadingWrapper = styled.div` + display: flex; + flex-direction: column; + margin-left: ${(props) => props.theme.spaces[2] + 1}px; + padding-bottom: 12px; +`; + +const SuggestedWidgetContainer = styled.div` + display: flex; + flex-direction: column; + flex-grow: 1; + overflow: hidden; `; type WidgetBindingInfo = { @@ -49,6 +159,8 @@ type WidgetBindingInfo = { propertyName: string; widgetName: string; image?: string; + icon?: string; + existingImage?: string; }; export const WIDGET_DATA_FIELD_MAP: Record = { @@ -57,42 +169,56 @@ export const WIDGET_DATA_FIELD_MAP: Record = { propertyName: "listData", widgetName: "List", image: `${ASSETS_CDN_URL}/widgetSuggestion/list.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/list.svg`, + icon: listWidgetIconSvg, }, TABLE_WIDGET: { label: "tabledata", propertyName: "tableData", widgetName: "Table", image: `${ASSETS_CDN_URL}/widgetSuggestion/table.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/existing_table.svg`, + icon: tableWidgetIconSvg, }, TABLE_WIDGET_V2: { label: "tabledata", propertyName: "tableData", widgetName: "Table", image: `${ASSETS_CDN_URL}/widgetSuggestion/table.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/existing_table.svg`, + icon: tableWidgetIconSvg, }, CHART_WIDGET: { label: "chart-series-data-control", propertyName: "chartData", widgetName: "Chart", image: `${ASSETS_CDN_URL}/widgetSuggestion/chart.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/chart.svg`, + icon: chartWidgetIconSvg, }, SELECT_WIDGET: { label: "options", propertyName: "options", widgetName: "Select", image: `${ASSETS_CDN_URL}/widgetSuggestion/dropdown.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/dropdown.svg`, + icon: selectWidgetIconSvg, }, TEXT_WIDGET: { label: "text", propertyName: "text", widgetName: "Text", image: `${ASSETS_CDN_URL}/widgetSuggestion/text.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/text.svg`, + icon: textWidgetIconSvg, }, INPUT_WIDGET_V2: { label: "text", propertyName: "defaultText", widgetName: "Input", image: `${ASSETS_CDN_URL}/widgetSuggestion/input.svg`, + existingImage: `${ASSETS_CDN_URL}/widgetSuggestion/input.svg`, + icon: inputWidgetIconSvg, }, }; @@ -179,10 +305,66 @@ type SuggestedWidgetProps = { hasWidgets: boolean; }; +function renderHeading(heading: string, subHeading: string) { + return ( + + {heading} + {subHeading} + + ); +} + +function renderWidgetItem( + icon: string | undefined, + name: string | undefined, + textKind: TextKind, +) { + return ( + + {icon && widget-icon} + + {name} + + + ); +} + +function renderWidgetImage(image: string | undefined) { + if (!!image) { + return widget-info-image; + } + return null; +} + function SuggestedWidgets(props: SuggestedWidgetProps) { const dispatch = useDispatch(); const dataTree = useSelector(getDataTree); const canvasWidgets = useSelector(getWidgets); + const applicationId = useSelector(getCurrentApplicationId); + const user = useSelector(getCurrentUser); + const { + isOpened: isWalkthroughOpened, + popFeature, + pushFeature, + } = useContext(WalkthroughContext) || {}; + + // A/B feature flag for query binding. + const isEnabledForQueryBinding = useSelector((state) => + selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled), + ); + + const params = useParams<{ + pageId: string; + apiId?: string; + queryId?: string; + }>(); + + const closeWalkthrough = async () => { + if (isWalkthroughOpened) { + popFeature && popFeature(); + await setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_binding_enabled, true); + } + }; const addWidget = ( suggestedWidget: SuggestedWidget, @@ -202,45 +384,216 @@ function SuggestedWidgets(props: SuggestedWidgetProps) { AnalyticsUtil.logEvent("SUGGESTED_WIDGET_CLICK", { widget: suggestedWidget.type, + [AB_TESTING_EVENT_KEYS.abTestingFlagLabel]: + FEATURE_FLAG.ab_ds_binding_enabled, + [AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForQueryBinding, + isWalkthroughOpened, }); + closeWalkthrough(); dispatch(addSuggestedWidget(payload)); }; - const label = props.hasWidgets + const handleBindData = (widgetId: string) => { + dispatch( + bindDataOnCanvas({ + queryId: (params.apiId || params.queryId) as string, + applicationId: applicationId as string, + pageId: params.pageId, + }), + ); + + closeWalkthrough(); + dispatch( + bindDataToWidget({ + widgetId: widgetId, + }), + ); + }; + + const isTableWidgetPresentOnCanvas = () => { + const canvasWidgetLength = Object.keys(canvasWidgets).length; + return ( + // widgetKey == 0 condition represents MainContainer + canvasWidgetLength > 1 && + Object.keys(canvasWidgets).some((widgetKey: string) => { + return ( + canvasWidgets[widgetKey]?.type === "TABLE_WIDGET_V2" && + parseInt(widgetKey, 0) !== 0 + ); + }) + ); + }; + + const labelOld = props.hasWidgets ? createMessage(ADD_NEW_WIDGET) : createMessage(SUGGESTED_WIDGETS); + const labelNew = createMessage(BINDING_SECTION_LABEL); + const addNewWidgetLabel = createMessage(ADD_NEW_WIDGET); + const addNewWidgetSubLabel = createMessage(ADD_NEW_WIDGET_SUB_HEADING); + const connectExistingWidgetLabel = createMessage( + CONNECT_EXISTING_WIDGET_LABEL, + ); + const connectExistingWidgetSubLabel = createMessage( + CONNECT_EXISTING_WIDGET_SUB_HEADING, + ); + const isWidgetsPresentOnCanvas = Object.keys(canvasWidgets).length > 0; + + const checkAndShowWalkthrough = async () => { + const isFeatureWalkthroughShown = await getFeatureFlagShownStatus( + FEATURE_FLAG.ab_ds_binding_enabled, + ); + + const isNewUser = user && (await isUserSignedUpFlagSet(user.email)); + // Adding walkthrough tutorial + isNewUser && + !isFeatureWalkthroughShown && + pushFeature && + 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, + true, + ); + }, + details: { + title: createMessage(BINDING_WALKTHROUGH_TITLE), + description: createMessage(BINDING_WALKTHROUGH_DESC), + imageURL: BINDING_GUIDE_GIF, + }, + offset: { + position: "left", + left: -40, + highlightPad: 5, + indicatorLeft: -3, + }, + eventParams: { + [AB_TESTING_EVENT_KEYS.abTestingFlagLabel]: + FEATURE_FLAG.ab_ds_binding_enabled, + [AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForQueryBinding, + }, + }); + }; + + useEffect(() => { + if (isEnabledForQueryBinding) checkAndShowWalkthrough(); + }, [isEnabledForQueryBinding]); return ( - - - {props.suggestedWidgets.map((suggestedWidget) => { - const widgetInfo: WidgetBindingInfo | undefined = - WIDGET_DATA_FIELD_MAP[suggestedWidget.type]; + + {!!isEnabledForQueryBinding ? ( + + {isTableWidgetPresentOnCanvas() && ( + + {renderHeading( + connectExistingWidgetLabel, + connectExistingWidgetSubLabel, + )} + {!isWidgetsPresentOnCanvas && ( + {createMessage(NO_EXISTING_WIDGETS)} + )} - if (!widgetInfo) return null; + {/* Table Widget condition is added temporarily as connect to existing + functionality is currently working only for Table Widget, + in future we want to support it for all widgets */} + { + + {Object.keys(canvasWidgets).map((widgetKey) => { + const widget: FlattenedWidgetProps | undefined = + canvasWidgets[widgetKey]; + const widgetInfo: WidgetBindingInfo | undefined = + WIDGET_DATA_FIELD_MAP[widget.type]; - return ( -
addWidget(suggestedWidget, widgetInfo)} - > - -
- {widgetInfo.image && ( - widget-info-image - )} + if (!widgetInfo || widget?.type !== "TABLE_WIDGET_V2") + return null; + + return ( +
handleBindData(widgetKey)} + > + +
+ {renderWidgetImage(widgetInfo.existingImage)} + {renderWidgetItem( + widgetInfo.icon, + widget.widgetName, + "body-s", + )} +
+
+
+ ); + })} + + } + + )} + + {renderHeading(addNewWidgetLabel, addNewWidgetSubLabel)} + + {props.suggestedWidgets.map((suggestedWidget) => { + const widgetInfo: WidgetBindingInfo | undefined = + WIDGET_DATA_FIELD_MAP[suggestedWidget.type]; + + if (!widgetInfo) return null; + + return ( +
addWidget(suggestedWidget, widgetInfo)} + > + + {renderWidgetItem( + widgetInfo.icon, + widgetInfo.widgetName, + "body-m", + )} + +
+ ); + })} +
+
+ + ) : ( + + + {props.suggestedWidgets.map((suggestedWidget) => { + const widgetInfo: WidgetBindingInfo | undefined = + WIDGET_DATA_FIELD_MAP[suggestedWidget.type]; + + if (!widgetInfo) return null; + + return ( +
addWidget(suggestedWidget, widgetInfo)} + > + +
+ {renderWidgetImage(widgetInfo.image)} +
+
- -
- ); - })} - - + ); + })} + + + )} + ); } diff --git a/app/client/src/components/editorComponents/ActionRightPane/index.tsx b/app/client/src/components/editorComponents/ActionRightPane/index.tsx index 619f98415b..561befe214 100644 --- a/app/client/src/components/editorComponents/ActionRightPane/index.tsx +++ b/app/client/src/components/editorComponents/ActionRightPane/index.tsx @@ -1,8 +1,8 @@ -import React, { useMemo } from "react"; +import React, { useContext, useMemo } from "react"; import styled from "styled-components"; import { Collapse, Classes as BPClasses } from "@blueprintjs/core"; import { Classes, getTypographyByKey } from "design-system-old"; -import { Button, Icon, Link } from "design-system"; +import { Button, Divider, Icon, Link, Text } from "design-system"; import { useState } from "react"; import Connections from "./Connections"; import SuggestedWidgets from "./SuggestedWidgets"; @@ -17,8 +17,12 @@ import type { AppState } from "@appsmith/reducers"; import { getDependenciesFromInverseDependencies } from "../Debugger/helpers"; import { BACK_TO_CANVAS, + BINDINGS_DISABLED_TOOLTIP, + BINDING_SECTION_LABEL, createMessage, NO_CONNECTIONS, + SCHEMA_WALKTHROUGH_DESC, + SCHEMA_WALKTHROUGH_TITLE, } from "@appsmith/constants/messages"; import type { SuggestedWidget, @@ -31,16 +35,39 @@ import { } from "selectors/editorSelectors"; import { builderURL } from "RouteBuilder"; import { hasManagePagePermission } from "@appsmith/utils/permissionHelpers"; +import DatasourceStructureHeader from "pages/Editor/Explorer/Datasources/DatasourceStructureHeader"; +import { DatasourceStructureContainer as DataStructureList } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer"; +import { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer"; +import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors"; +import { + AB_TESTING_EVENT_KEYS, + FEATURE_FLAG, +} from "@appsmith/entities/FeatureFlag"; +import { + getDatasourceStructureById, + getPluginDatasourceComponentFromId, + getPluginNameFromId, +} from "selectors/entitiesSelector"; +import { DatasourceComponentTypes } from "api/PluginApi"; +import { fetchDatasourceStructure } from "actions/datasourceActions"; +import WalkthroughContext from "components/featureWalkthrough/walkthroughContext"; +import { + getFeatureFlagShownStatus, + isUserSignedUpFlagSet, + setFeatureFlagShownStatus, +} from "utils/storage"; +import { PluginName } from "entities/Action"; +import { getCurrentUser } from "selectors/usersSelectors"; +import { Tooltip } from "design-system"; +import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants"; + +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%; - -webkit-animation: slide-left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; - animation: slide-left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; - - & > div { - margin-top: ${(props) => props.theme.spaces[11]}px; - } & > a { margin-top: 0; @@ -90,15 +117,30 @@ const SideBar = styled.div` const BackToCanvasLink = styled(Link)` margin-left: ${(props) => props.theme.spaces[1] + 1}px; margin-top: ${(props) => props.theme.spaces[11]}px; + margin-bottom: ${(props) => props.theme.spaces[11]}px; `; const Label = styled.span` cursor: pointer; `; -const CollapsibleWrapper = styled.div<{ isOpen: boolean }>` +const CollapsibleWrapper = styled.div<{ + isOpen: boolean; + isDisabled?: boolean; +}>` + display: flex; + flex-direction: column; + overflow: hidden; + ${(props) => !!props.isDisabled && `opacity: 0.6`}; + + &&&&&& .${BPClasses.COLLAPSE} { + flex-grow: 1; + overflow-y: auto !important; + } + .${BPClasses.COLLAPSE_BODY} { padding-top: ${(props) => props.theme.spaces[3]}px; + height: 100%; } & > .icon-text:first-child { @@ -142,14 +184,36 @@ const Placeholder = styled.div` text-align: center; `; +const DataStructureListWrapper = styled.div` + overflow-y: scroll; + height: 100%; +`; + +const SchemaSideBarSection = styled.div<{ height: number; marginTop?: number }>` + margin-top: ${(props) => props?.marginTop && `${props.marginTop}px`}; + height: auto; + display: flex; + width: 100%; + flex-direction: column; + ${(props) => props.height && `max-height: ${props.height}%;`} +`; + type CollapsibleProps = { expand?: boolean; children: ReactNode; label: string; + customLabelComponent?: JSX.Element; + isDisabled?: boolean; +}; + +type DisabledCollapsibleProps = { + label: string; + tooltipLabel?: string; }; export function Collapsible({ children, + customLabelComponent, expand = true, label, }: CollapsibleProps) { @@ -162,8 +226,14 @@ export function Collapsible({ return ( {children} @@ -172,6 +242,24 @@ export function Collapsible({ ); } +export function DisabledCollapsible({ + label, + tooltipLabel = "", +}: DisabledCollapsibleProps) { + return ( + + + + + + ); +} + export function useEntityDependencies(actionName: string) { const deps = useSelector((state: AppState) => state.evaluations.dependencies); const entityDependencies = useMemo( @@ -194,9 +282,12 @@ export function useEntityDependencies(actionName: string) { function ActionSidebar({ actionName, + context, + datasourceId, entityDependencies, hasConnections, hasResponse, + pluginId, suggestedWidgets, }: { actionName: string; @@ -207,11 +298,16 @@ function ActionSidebar({ directDependencies: string[]; inverseDependencies: string[]; } | null; + datasourceId: string; + pluginId: string; + context: DatasourceStructureContext; }) { const dispatch = useDispatch(); const widgets = useSelector(getWidgets); const applicationId = useSelector(getCurrentApplicationId); const pageId = useSelector(getCurrentPageId); + const user = useSelector(getCurrentUser); + const { pushFeature } = useContext(WalkthroughContext) || {}; const params = useParams<{ pageId: string; apiId?: string; @@ -232,6 +328,100 @@ function ActionSidebar({ ); }; + const pluginName = useSelector((state) => + getPluginNameFromId(state, pluginId || ""), + ); + + const pluginDatasourceForm = useSelector((state) => + getPluginDatasourceComponentFromId(state, pluginId || ""), + ); + + // A/B feature flag for datasource structure. + const isEnabledForDSSchema = useSelector((state) => + selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_schema_enabled), + ); + + // A/B feature flag for query binding. + const isEnabledForQueryBinding = useSelector((state) => + selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled), + ); + + const datasourceStructure = useSelector((state) => + getDatasourceStructureById(state, datasourceId), + ); + + useEffect(() => { + if ( + datasourceId && + datasourceStructure === undefined && + pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm + ) { + dispatch( + fetchDatasourceStructure( + datasourceId, + true, + DatasourceStructureContext.QUERY_EDITOR, + ), + ); + } + }, []); + + const checkAndShowWalkthrough = async () => { + const isFeatureWalkthroughShown = await getFeatureFlagShownStatus( + FEATURE_FLAG.ab_ds_schema_enabled, + ); + + const isNewUser = user && (await isUserSignedUpFlagSet(user.email)); + // Adding walkthrough tutorial + isNewUser && + !isFeatureWalkthroughShown && + pushFeature && + 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, + true, + ); + }, + details: { + title: createMessage(SCHEMA_WALKTHROUGH_TITLE), + description: createMessage(SCHEMA_WALKTHROUGH_DESC), + imageURL: SCHEMA_GUIDE_GIF, + }, + offset: { + position: "left", + left: -40, + highlightPad: 5, + indicatorLeft: -3, + style: { + transform: "none", + }, + }, + eventParams: { + [AB_TESTING_EVENT_KEYS.abTestingFlagLabel]: + FEATURE_FLAG.ab_ds_schema_enabled, + [AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema, + }, + }); + }; + + const showSchema = + isEnabledForDSSchema && + pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm && + pluginName !== PluginName.SMTP; + + useEffect(() => { + if (showSchema) { + checkAndShowWalkthrough(); + } + }, [showSchema]); + const hasWidgets = Object.keys(widgets).length > 1; const pagePermissions = useSelector(getPagePermissions); @@ -242,7 +432,13 @@ function ActionSidebar({ canEditPage && hasResponse && suggestedWidgets && !!suggestedWidgets.length; const showSnipingMode = hasResponse && hasWidgets; - if (!hasConnections && !showSuggestedWidgets && !showSnipingMode) { + if ( + !hasConnections && + !showSuggestedWidgets && + !showSnipingMode && + // putting this here to make the placeholder only appear for rest APIs. + pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm + ) { return {createMessage(NO_CONNECTIONS)}; } @@ -257,33 +453,69 @@ function ActionSidebar({ {createMessage(BACK_TO_CANVAS)} - {hasConnections && ( + {showSchema && ( + + + } + expand={!showSuggestedWidgets} + label="Schema" + > + + + + + + )} + + {showSchema && isEnabledForQueryBinding && } + + {hasConnections && !isEnabledForQueryBinding && ( )} - {canEditPage && hasResponse && Object.keys(widgets).length > 1 && ( - - {/*
Go to canvas and select widgets
*/} - - - -
- )} - {showSuggestedWidgets && ( - + {!isEnabledForQueryBinding && + canEditPage && + hasResponse && + Object.keys(widgets).length > 1 && ( + + + + + + )} + {showSuggestedWidgets ? ( + + + + ) : ( + isEnabledForQueryBinding && ( + + ) )} ); diff --git a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx index 3b323493a4..258bd9cdde 100644 --- a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx +++ b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx @@ -45,8 +45,8 @@ export function useTableOrSpreadsheet() { const isFetchingSpreadsheets = useSelector(getIsFetchingGsheetSpreadsheets); - const isFetchingDatasourceStructure = useSelector( - getIsFetchingDatasourceStructure, + const isFetchingDatasourceStructure = useSelector((state: AppState) => + getIsFetchingDatasourceStructure(state, config.datasource), ); const selectedDatasourcePluginPackageName = useSelector((state: AppState) => diff --git a/app/client/src/components/featureWalkthrough/index.tsx b/app/client/src/components/featureWalkthrough/index.tsx new file mode 100644 index 0000000000..eedbbd6195 --- /dev/null +++ b/app/client/src/components/featureWalkthrough/index.tsx @@ -0,0 +1,82 @@ +import React, { lazy, useEffect, useState, Suspense } from "react"; +import type { FeatureParams } from "./walkthroughContext"; +import WalkthroughContext from "./walkthroughContext"; +import { createPortal } from "react-dom"; +import { hideIndicator } from "pages/Editor/GuidedTour/utils"; +import { retryPromise } from "utils/AppsmithUtils"; +import { useLocation } from "react-router-dom"; + +const WalkthroughRenderer = lazy(() => { + return retryPromise( + () => + import( + /* webpackChunkName: "walkthrough-renderer" */ "./walkthroughRenderer" + ), + ); +}); + +const LoadingFallback = () => null; + +export default function Walkthrough({ children }: any) { + const [activeWalkthrough, setActiveWalkthrough] = + useState(); + const [feature, setFeature] = useState([]); + const location = useLocation(); + + const pushFeature = (value: FeatureParams) => { + const alreadyExists = feature.some((f) => f.targetId === value.targetId); + if (!alreadyExists) { + if (Array.isArray(value)) { + setFeature((e) => [...e, ...value]); + } else { + setFeature((e) => [...e, value]); + } + } + updateActiveWalkthrough(); + }; + + const popFeature = () => { + hideIndicator(); + setFeature((e) => { + e.shift(); + return [...e]; + }); + }; + + const updateActiveWalkthrough = () => { + if (feature.length > 0) { + const highlightArea = document.querySelector(`#${feature[0].targetId}`); + if (highlightArea) { + setActiveWalkthrough(feature[0]); + } else { + setActiveWalkthrough(null); + } + } else { + setActiveWalkthrough(null); + } + }; + + useEffect(() => { + if (feature.length > -1) updateActiveWalkthrough(); + }, [feature.length, location]); + + return ( + + {children} + {activeWalkthrough && + createPortal( + }> + + , + document.body, + )} + + ); +} diff --git a/app/client/src/components/featureWalkthrough/utils.ts b/app/client/src/components/featureWalkthrough/utils.ts new file mode 100644 index 0000000000..9f737542ea --- /dev/null +++ b/app/client/src/components/featureWalkthrough/utils.ts @@ -0,0 +1,92 @@ +import type { OffsetType, PositionType } from "./walkthroughContext"; + +const DEFAULT_POSITION: PositionType = "top"; +export const PADDING_HIGHLIGHT = 10; + +type PositionCalculator = { + offset?: OffsetType; + targetId: string; +}; + +export function getPosition({ offset, targetId }: PositionCalculator) { + const target = document.querySelector(`#${targetId}`); + const bodyCoordinates = document.body.getBoundingClientRect(); + if (!target) return null; + let coordinates; + if (target) { + coordinates = target.getBoundingClientRect(); + } + + if (!coordinates) return null; + + const offsetValues = { top: offset?.top || 0, left: offset?.left || 0 }; + const extraStyles = offset?.style || {}; + + /** + * . - - - - - - - - - - - - - - - - - . + * | Body | + * | | + * | . - - - - - - - - - - . | + * | | Offset | | + * | | . - - - - - - - . | | + * | | | / / / / / / / | | | + * | | | / / /Target/ /| | | + * | | | / / / / / / / | | | + * | | . - - - - - - - . | | + * | | | | + * | . _ _ _ _ _ _ _ _ _ _ . | + * | | + * . - - - - - - - - - - - - - - - - - . + */ + + switch (offset?.position || DEFAULT_POSITION) { + case "top": + return { + bottom: + bodyCoordinates.height - + coordinates.top - + offsetValues.top + + PADDING_HIGHLIGHT + + "px", + left: coordinates.left + offsetValues.left + PADDING_HIGHLIGHT + "px", + transform: "translateX(-50%)", + ...extraStyles, + }; + case "bottom": + return { + top: + coordinates.height + + coordinates.top + + offsetValues.top + + PADDING_HIGHLIGHT + + "px", + left: coordinates.left + offsetValues.left - PADDING_HIGHLIGHT + "px", + transform: "translateX(-50%)", + ...extraStyles, + }; + case "left": + return { + top: coordinates.top + offsetValues.top - PADDING_HIGHLIGHT + "px", + right: + bodyCoordinates.width - + coordinates.left - + offsetValues.left + + PADDING_HIGHLIGHT + + "px", + transform: "translateY(-50%)", + ...extraStyles, + }; + case "right": + return { + top: coordinates.top + offsetValues.top - PADDING_HIGHLIGHT + "px", + left: + coordinates.left + + coordinates.width + + offsetValues.left + + PADDING_HIGHLIGHT + + "px", + transform: "translateY(-50%)", + ...extraStyles, + }; + } +} diff --git a/app/client/src/components/featureWalkthrough/walkthroughContext.tsx b/app/client/src/components/featureWalkthrough/walkthroughContext.tsx new file mode 100644 index 0000000000..9d31d38eef --- /dev/null +++ b/app/client/src/components/featureWalkthrough/walkthroughContext.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +export type PositionType = "top" | "bottom" | "left" | "right"; + +export type OffsetType = { + // Position for the instructions and indicator + position?: PositionType; + // Adds an offset to top or bottom properties (of Instruction div) depending upon the position + top?: number; + // Adds an offset to left or right properties (of Instruction div) depending upon the position + left?: number; + // Style for the Instruction div overrides all other styles + style?: any; + // Indicator top and left offsets + indicatorTop?: number; + indicatorLeft?: number; + // container offset for highlight + highlightPad?: number; +}; + +export type FeatureDetails = { + // Title to show on the instruction screen + title: string; + // Description to show on the instruction screen + description: string; + // Gif or Image to give a walkthrough + imageURL?: string; +}; + +export type FeatureParams = { + // To execute a function on dismissing the tutorial walkthrough. + onDismiss?: () => void; + // Target Id without # to highlight the feature + targetId: string; + // Details for the instruction screen + details?: FeatureDetails; + // Offsets for the instruction screen and the indicator + offset?: OffsetType; + // Event params + eventParams?: Record; +}; + +type WalkthroughContextType = { + pushFeature: (feature: FeatureParams) => void; + popFeature: () => void; + feature: FeatureParams[]; + isOpened: boolean; +}; + +const WalkthroughContext = React.createContext< + WalkthroughContextType | undefined +>(undefined); + +export default WalkthroughContext; diff --git a/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx b/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx new file mode 100644 index 0000000000..97f52dfec2 --- /dev/null +++ b/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx @@ -0,0 +1,258 @@ +import { Icon, Text } from "design-system"; +import { showIndicator } from "pages/Editor/GuidedTour/utils"; +import React, { useContext, useEffect, useState } from "react"; +import styled from "styled-components"; +import { PADDING_HIGHLIGHT, getPosition } from "./utils"; +import type { + FeatureDetails, + FeatureParams, + OffsetType, +} from "./walkthroughContext"; +import WalkthroughContext from "./walkthroughContext"; +import AnalyticsUtil from "utils/AnalyticsUtil"; + +const CLIPID = "clip__feature"; +const Z_INDEX = 1000; + +const WalkthroughWrapper = styled.div` + left: 0px; + top: 0px; + position: fixed; + width: 100%; + height: 100%; + color: rgb(0, 0, 0, 0.7); + z-index: ${Z_INDEX}; + // This allows the user to click on the target element rather than the overlay div + pointer-events: none; +`; + +const SvgWrapper = styled.svg` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +`; + +const InstructionsWrapper = styled.div` + padding: var(--ads-v2-spaces-4); + position: absolute; + background: white; + display: flex; + flex-direction: column; + width: 296px; + pointer-events: auto; + border-radius: var(--ads-radius-1); +`; + +const ImageWrapper = styled.div` + border-radius: var(--ads-radius-1); + background: #f1f5f9; + display: flex; + align-items: center; + justify-content: center; + margin-top: 8px; + padding: var(--ads-v2-spaces-7); + img { + max-height: 220px; + } +`; + +const InstructionsHeaderWrapper = styled.div` + display: flex; + p { + flex-grow: 1; + } + span { + align-self: flex-start; + margin-top: 5px; + cursor: pointer; + } +`; + +type RefRectParams = { + // body params + bh: number; + bw: number; + // target params + th: number; + tw: number; + tx: number; + ty: number; +}; + +/* + * Clip Path Polygon : + * 1) 0 0 ----> (body start) (body start) + * 2) 0 ${boundingRect.bh} ----> (body start) (body end) + * 3) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end) + * 4) ${boundingRect.tx} ${boundingRect.ty} ----> (target start) (target start) + * 5) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty} ----> (target end) (target start) + * 6) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty + boundingRect.th} ----> (target end) (target end) + * 7) ${boundingRect.tx} ${boundingRect.ty + boundingRect.th} ----> (target start) (target end) + * 8) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end) + * 9) ${boundingRect.bw} ${boundingRect.bh} ----> (body end) (body end) + * 10) ${boundingRect.bw} 0 ----> (body end) (body start) + * + * + * 1 ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ←10 + * ↓ ↑ + * ↓ Body ↑ + * ↓ ↑ + * ↓ ↑ + * ↓ 4 → → → → → → → 5 ↑ + * ↓ ↑ / / / / / / / ↓ ↑ + * ↓ ↑ / / /Target/ /↓ ↑ + * ↓ ↑ / / / / / / / ↓ ↑ + * ↓ 7 ← ← ← ← ← ← ← 6 ↑ + * ↓ ↑↓ ↑ + * ↓ ↑↓ ↑ + * 2 → → → → 3,8 → → → → → → → → → → → 9 + */ + +/** + * Creates a Highlighting Clipping mask around a target container + * @param targetId Id for the target container to show highlighting around it + */ + +const WalkthroughRenderer = ({ + details, + offset, + onDismiss, + targetId, + eventParams = {}, +}: FeatureParams) => { + const [boundingRect, setBoundingRect] = useState(null); + const { popFeature } = useContext(WalkthroughContext) || {}; + 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, + }); + } + }; + + useEffect(() => { + updateBoundingRect(); + const highlightArea = document.querySelector(`#${targetId}`); + AnalyticsUtil.logEvent("WALKTHROUGH_SHOWN", eventParams); + window.addEventListener("resize", updateBoundingRect); + const resizeObserver = new ResizeObserver(updateBoundingRect); + if (highlightArea) { + resizeObserver.observe(highlightArea); + } + return () => { + window.removeEventListener("resize", updateBoundingRect); + if (highlightArea) resizeObserver.unobserve(highlightArea); + }; + }, [targetId]); + + const onDismissWalkthrough = () => { + onDismiss && onDismiss(); + popFeature && popFeature(); + }; + + if (!boundingRect) return null; + + return ( + + + + + + + + + + + + ); +}; + +const InstructionsComponent = ({ + details, + offset, + onClose, + targetId, +}: { + details?: FeatureDetails; + offset?: OffsetType; + targetId: string; + onClose: () => void; +}) => { + if (!details) return null; + + const positionAttr = getPosition({ + targetId, + offset, + }); + + return ( + + + + {details.title} + + + + {details.description} + {details.imageURL && ( + + + + )} + + ); +}; + +export default WalkthroughRenderer; diff --git a/app/client/src/components/formControls/WhereClauseControl.tsx b/app/client/src/components/formControls/WhereClauseControl.tsx index 99008248c6..9e5f622fdc 100644 --- a/app/client/src/components/formControls/WhereClauseControl.tsx +++ b/app/client/src/components/formControls/WhereClauseControl.tsx @@ -282,7 +282,7 @@ function ConditionComponent(props: any, index: number) { props.onDeletePressed(index); }} size="md" - startIcon="cross-line" + startIcon="close" /> ); @@ -397,7 +397,7 @@ function ConditionBlock(props: any) { onDeletePressed(index); }} size="md" - startIcon="cross-line" + startIcon="close" top={"24px"} /> diff --git a/app/client/src/constants/Datasource.ts b/app/client/src/constants/Datasource.ts index 3cb14ebb6b..4228996469 100644 --- a/app/client/src/constants/Datasource.ts +++ b/app/client/src/constants/Datasource.ts @@ -24,6 +24,7 @@ export const DatasourceCreateEntryPoints = { export const DatasourceEditEntryPoints = { DATASOURCE_CARD_EDIT: "DATASOURCE_CARD_EDIT", DATASOURCE_FORM_EDIT: "DATASOURCE_FORM_EDIT", + QUERY_EDITOR_DATASOURCE_SCHEMA: "QUERY_EDITOR_DATASOURCE_SCHEMA", }; export const DB_QUERY_DEFAULT_TABLE_NAME = "<>"; diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index b456935216..a00e9542f6 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -3015,7 +3015,7 @@ export const theme: Theme = { }, }, actionSidePane: { - width: 265, + width: 280, }, onboarding: { statusBarHeight: 92, diff --git a/app/client/src/entities/Action/index.ts b/app/client/src/entities/Action/index.ts index 558a9c1742..faef9e3fb9 100644 --- a/app/client/src/entities/Action/index.ts +++ b/app/client/src/entities/Action/index.ts @@ -40,6 +40,7 @@ export enum PluginName { SNOWFLAKE = "Snowflake", ARANGODB = "ArangoDB", REDSHIFT = "Redshift", + SMTP = "SMTP", } export enum PaginationType { diff --git a/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx b/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx index 48ce5ac333..fc3908231a 100644 --- a/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx +++ b/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx @@ -15,6 +15,7 @@ import { useDispatch, useSelector } from "react-redux"; import { getApiRightPaneSelectedTab } from "selectors/apiPaneSelectors"; import isUndefined from "lodash/isUndefined"; import { Button, Tab, TabPanel, Tabs, TabsList, Tag } from "design-system"; +import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer"; import type { Datasource } from "entities/Datasource"; import { getCurrentEnvironment } from "@appsmith/utils/Environments"; @@ -125,7 +126,7 @@ const DataSourceNameContainer = styled.div` const SomeWrapper = styled.div` height: 100%; - padding: 0 var(--ads-v2-spaces-6); + padding: 0 var(--ads-v2-spaces-4); `; const NoEntityFoundWrapper = styled.div` @@ -311,9 +312,12 @@ function ApiRightPane(props: any) { diff --git a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx index 5ec1d3ba8d..e123b6278d 100644 --- a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx +++ b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx @@ -735,9 +735,11 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) { applicationId={props.applicationId} currentActionDatasourceId={currentActionDatasourceId} currentPageId={props.currentPageId} + datasourceId={props.currentActionDatasourceId} datasources={props.datasources} hasResponse={props.hasResponse} onClick={updateDatasource} + pluginId={props.pluginId} suggestedWidgets={props.suggestedWidgets} /> diff --git a/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx b/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx index cc03bc7eb3..607977e416 100644 --- a/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx +++ b/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx @@ -24,7 +24,7 @@ import { Spinner } from "design-system"; import LogoInput from "@appsmith/pages/Editor/NavigationSettings/LogoInput"; import SwitchSettingForLogoConfiguration from "./SwitchSettingForLogoConfiguration"; import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; -import { useFeatureFlagCheck } from "selectors/featureFlagsSelectors"; +import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors"; /** * TODO - @Dhruvik - ImprovedAppNav @@ -48,8 +48,8 @@ export type LogoConfigurationSwitches = { function NavigationSettings() { const application = useSelector(getCurrentApplication); const applicationId = useSelector(getCurrentApplicationId); - const isAppLogoEnabled = useFeatureFlagCheck( - FEATURE_FLAG.APP_NAVIGATION_LOGO_UPLOAD, + const isAppLogoEnabled = useSelector((state) => + selectFeatureFlagCheck(state, FEATURE_FLAG.APP_NAVIGATION_LOGO_UPLOAD), ); const dispatch = useDispatch(); const [navigationSetting, setNavigationSetting] = useState( diff --git a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx index 79c083396f..21bc0fe54d 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx @@ -38,12 +38,13 @@ type Props = DatasourceDBEditorProps & export const Form = styled.form<{ showFilterComponent: boolean; + viewMode: boolean; }>` display: flex; flex-direction: column; - height: ${({ theme }) => `calc(100% - ${theme.backBanner})`}; + ${(props) => + !props.viewMode && `height: ${`calc(100% - ${props?.theme.backBanner})`};`} overflow-y: scroll; - flex: 8 8 80%; padding-bottom: 20px; margin-left: ${(props) => (props.showFilterComponent ? "24px" : "0px")}; `; @@ -90,6 +91,7 @@ class DatasourceDBEditor extends JSONtoForm { e.preventDefault(); }} showFilterComponent={showFilterComponent} + viewMode={viewMode} > {messages && messages.map((msg, i) => { diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx index 71206ca1ea..ebf309e138 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx @@ -72,6 +72,7 @@ interface DatasourceRestApiEditorProps { toggleSaveActionFlag: (flag: boolean) => void; triggerSave?: boolean; datasourceDeleteTrigger: () => void; + viewMode: boolean; } type Props = DatasourceRestApiEditorProps & @@ -247,6 +248,7 @@ class DatasourceRestAPIEditor extends React.Component { e.preventDefault(); }} showFilterComponent={this.props.showFilterComponent} + viewMode={this.props.viewMode} > {this.renderEditor()} diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx index 34881681b9..36050495bd 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx @@ -32,7 +32,6 @@ import type { RouteComponentProps } from "react-router"; import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane"; import { DatasourceComponentTypes } from "api/PluginApi"; import DatasourceSaasForm from "../SaaSEditor/DatasourceForm"; - import { getCurrentApplicationId, getPagePermissions, @@ -493,51 +492,49 @@ class DatasourceEditorRouter extends React.Component { } = this.props; const shouldViewMode = viewMode && !isInsideReconnectModal; - // Check for specific form types first - if ( - pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm && - !shouldViewMode - ) { - return ( - <> - - {this.renderSaveDisacardModal()} - - ); - } - // Default to DB Editor Form return ( <> - + { + // Check for specific form types first + pluginDatasourceForm === + DatasourceComponentTypes.RestAPIDatasourceForm && + !shouldViewMode ? ( + + ) : ( + // Default to DB Editor Form + + ) + } {this.renderSaveDisacardModal()} ); diff --git a/app/client/src/pages/Editor/Explorer/Datasources.tsx b/app/client/src/pages/Editor/Explorer/Datasources.tsx index b4741526be..fbdf4adb9e 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources.tsx @@ -123,11 +123,11 @@ const Datasources = React.memo(() => { { - dispatch(refreshDatasourceStructure(props.datasourceId)); + dispatch( + refreshDatasourceStructure( + props.datasourceId, + DatasourceStructureContext.EXPLORER, + ), + ); }, [dispatch, props.datasourceId]); const [confirmDelete, setConfirmDelete] = useState(false); diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx index d18e39cf78..c6616c49f6 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx @@ -13,9 +13,16 @@ import { } from "actions/datasourceActions"; import { useDispatch, useSelector } from "react-redux"; import type { AppState } from "@appsmith/reducers"; -import { DatasourceStructureContainer } from "./DatasourceStructureContainer"; +import { + DatasourceStructureContainer, + DatasourceStructureContext, +} from "./DatasourceStructureContainer"; import { isStoredDatasource, PluginType } from "entities/Action"; -import { getAction } from "selectors/entitiesSelector"; +import { + getAction, + getDatasourceStructureById, + getIsFetchingDatasourceStructure, +} from "selectors/entitiesSelector"; import { datasourcesEditorIdURL, saasEditorDatasourceIdURL, @@ -81,13 +88,13 @@ const ExplorerDatasourceEntity = React.memo( const updateDatasourceNameCall = (id: string, name: string) => updateDatasourceName({ id: props.datasource.id, name }); - const datasourceStructure = useSelector((state: AppState) => { - return state.entities.datasources.structure[props.datasource.id]; - }); + const datasourceStructure = useSelector((state: AppState) => + getDatasourceStructureById(state, props.datasource.id), + ); - const isFetchingDatasourceStructure = useSelector((state: AppState) => { - return state.entities.datasources.fetchingDatasourceStructure; - }); + const isFetchingDatasourceStructure = useSelector((state: AppState) => + getIsFetchingDatasourceStructure(state, props.datasource.id), + ); const expandDatasourceId = useSelector((state: AppState) => { return state.ui.datasourcePane.expandDatasourceId; @@ -95,7 +102,13 @@ const ExplorerDatasourceEntity = React.memo( //Debounce fetchDatasourceStructure request. const debounceFetchDatasourceRequest = debounce(async () => { - dispatch(fetchDatasourceStructure(props.datasource.id, true)); + dispatch( + fetchDatasourceStructure( + props.datasource.id, + true, + DatasourceStructureContext.EXPLORER, + ), + ); }, 300); const getDatasourceStructure = useCallback( @@ -155,6 +168,7 @@ const ExplorerDatasourceEntity = React.memo( updateEntityName={updateDatasourceNameCall} > setActive(false)); + const { isOpened: isWalkthroughOpened, popFeature } = + useContext(WalkthroughContext) || {}; + const datasource = useSelector((state: AppState) => getDatasource(state, props.datasourceId), ); + const plugin: Plugin | undefined = useSelector((state) => + getPlugin(state, datasource?.pluginId || ""), + ); + const datasourcePermissions = datasource?.userPermissions || []; const pagePermissions = useSelector(getPagePermissions); @@ -44,62 +61,98 @@ export function DatasourceStructure(props: DatasourceStructureProps) { ...pagePermissions, ]); - const lightningMenu = canCreateDatasourceActions ? ( - - - - - ) : null; + + + ) : null; if (dbStructure.templates) templateMenu = lightningMenu; const columnsAndKeys = dbStructure.columns.concat(dbStructure.keys); return ( canCreateDatasourceActions && setActive(!active)} + action={onEntityClick} active={active} - className={`datasourceStructure`} + className={`datasourceStructure${ + props.context !== DatasourceStructureContext.EXPLORER && + `-${props.context}` + }`} contextMenu={templateMenu} - entityId={"DatasourceStructure"} + entityId={`${props.datasourceId}-${dbStructure.name}-${props.context}`} + forceExpand={props.forceExpand} icon={datasourceTableIcon} + isDefaultExpanded={props?.isDefaultOpen} name={dbStructure.name} step={props.step} > - {columnsAndKeys.map((field, index) => { - return ( - - ); - })} + <> + {columnsAndKeys.map((field, index) => { + return ( + + ); + })} + ); } diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx index 955f7c218b..9a69b4cbe4 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx @@ -1,57 +1,205 @@ import { createMessage, + DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT, SCHEMA_NOT_AVAILABLE, + TABLE_OR_COLUMN_NOT_FOUND, } from "@appsmith/constants/messages"; import type { DatasourceStructure as DatasourceStructureType, DatasourceTable, } from "entities/Datasource"; import type { ReactElement } from "react"; -import React, { memo } from "react"; +import React, { memo, useEffect, useMemo, useState } from "react"; import EntityPlaceholder from "../Entity/Placeholder"; -import { useEntityUpdateState } from "../hooks"; import DatasourceStructure from "./DatasourceStructure"; +import { Input, Text } from "design-system"; +import styled from "styled-components"; +import { getIsFetchingDatasourceStructure } from "selectors/entitiesSelector"; +import { useSelector } from "react-redux"; +import type { AppState } from "@appsmith/reducers"; +import DatasourceStructureLoadingContainer from "./DatasourceStructureLoadingContainer"; +import DatasourceStructureNotFound from "./DatasourceStructureNotFound"; +import AnalyticsUtil from "utils/AnalyticsUtil"; type Props = { datasourceId: string; datasourceStructure?: DatasourceStructureType; step: number; + context: DatasourceStructureContext; + pluginName?: string; + currentActionId?: string; }; +export enum DatasourceStructureContext { + EXPLORER = "entity-explorer", + QUERY_EDITOR = "query-editor", + // this does not exist yet, but in case it does in the future. + API_EDITOR = "api-editor", +} + +const DatasourceStructureSearchContainer = styled.div` + margin-bottom: 8px; + position: sticky; + top: 0; + overflow: hidden; + z-index: 10; + background: white; +`; + const Container = (props: Props) => { - const isLoading = useEntityUpdateState(props.datasourceId); - let view: ReactElement =
; + const isLoading = useSelector((state: AppState) => + getIsFetchingDatasourceStructure(state, props.datasourceId), + ); + let view: ReactElement | JSX.Element =
; + + const [datasourceStructure, setDatasourceStructure] = useState< + DatasourceStructureType | undefined + >(props.datasourceStructure); + const [hasSearchedOccured, setHasSearchedOccured] = useState(false); + + useEffect(() => { + if (datasourceStructure !== props.datasourceStructure) { + setDatasourceStructure(props.datasourceStructure); + } + }, [props.datasourceStructure]); + + const flatStructure = useMemo(() => { + if (!props.datasourceStructure?.tables?.length) return []; + const list: string[] = []; + + props.datasourceStructure.tables.map((table) => { + table.columns.forEach((column) => { + list.push(`${table.name}~${column.name}`); + }); + }); + + return list; + }, [props.datasourceStructure]); + + const handleOnChange = (value: string) => { + if (!props.datasourceStructure?.tables?.length) return; + + if (value.length > 0) { + !hasSearchedOccured && setHasSearchedOccured(true); + } else { + hasSearchedOccured && setHasSearchedOccured(false); + } + + const tables = new Set(); + const columns = new Set(); + + flatStructure.forEach((structure) => { + const segments = structure.split("~"); + // if the value is present in the columns, add the column and its parent table. + if (segments[1].toLowerCase().includes(value)) { + tables.add(segments[0]); + columns.add(segments[1]); + return; + } + + // if the value is present in the table but not in the columns, add the table + if (segments[0].toLowerCase().includes(value)) { + tables.add(segments[0]); + return; + } + }); + + const filteredDastasourceStructure = props.datasourceStructure.tables + .map((structure) => ({ + ...structure, + columns: + // if the size of the columns set is 0, then simply default to the entire column + columns.size === 0 + ? structure.columns + : structure.columns.filter((column) => columns.has(column.name)), + keys: + columns.size === 0 + ? structure.keys + : structure.keys.filter((key) => columns.has(key.name)), + })) + .filter((table) => tables.has(table.name)); + + setDatasourceStructure({ tables: filteredDastasourceStructure }); + + AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_SEARCH", { + datasourceId: props.datasourceId, + pluginName: props.pluginName, + }); + }; if (!isLoading) { if (props.datasourceStructure?.tables?.length) { view = ( <> - {props.datasourceStructure.tables.map( - (structure: DatasourceTable) => { + {props.context !== DatasourceStructureContext.EXPLORER && ( + + handleOnChange(value)} + placeholder={createMessage( + DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT, + )} + size={"md"} + startIcon="search" + type="text" + /> + + )} + {!!datasourceStructure?.tables?.length && + datasourceStructure.tables.map((structure: DatasourceTable) => { return ( ); - }, + })} + + {!datasourceStructure?.tables?.length && ( + + {createMessage(TABLE_OR_COLUMN_NOT_FOUND)} + )} ); } else { - view = ( - - {props.datasourceStructure && - props.datasourceStructure.error && - props.datasourceStructure.error.message && - props.datasourceStructure.error.message !== "null" - ? props.datasourceStructure.error.message - : createMessage(SCHEMA_NOT_AVAILABLE)} - - ); + if (props.context !== DatasourceStructureContext.EXPLORER) { + view = ( + + ); + } else { + view = ( + + {props.datasourceStructure && + props.datasourceStructure.error && + props.datasourceStructure.error.message && + props.datasourceStructure.error.message !== "null" + ? props.datasourceStructure.error.message + : createMessage(SCHEMA_NOT_AVAILABLE)} + + ); + } } + } else if ( + // intentionally leaving this here in case we want to show loading states in the explorer or query editor page + props.context !== DatasourceStructureContext.EXPLORER && + isLoading + ) { + view = ; } return view; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureHeader.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureHeader.tsx new file mode 100644 index 0000000000..bc3fa6d089 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureHeader.tsx @@ -0,0 +1,46 @@ +import React, { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { Icon, Text } from "design-system"; +import styled from "styled-components"; +import { refreshDatasourceStructure } from "actions/datasourceActions"; +import { SCHEMA_LABEL, createMessage } from "@appsmith/constants/messages"; +import { DatasourceStructureContext } from "./DatasourceStructureContainer"; + +type Props = { + datasourceId: string; +}; + +const HeaderWrapper = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +`; + +export default function DatasourceStructureHeader(props: Props) { + const dispatch = useDispatch(); + + const dispatchRefresh = useCallback( + (event: React.MouseEvent) => { + event.stopPropagation(); + dispatch( + refreshDatasourceStructure( + props.datasourceId, + DatasourceStructureContext.QUERY_EDITOR, + ), + ); + }, + [dispatch, props.datasourceId], + ); + + return ( + + + {createMessage(SCHEMA_LABEL)} + +
dispatchRefresh(event)}> + +
+
+ ); +} diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureLoadingContainer.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureLoadingContainer.tsx new file mode 100644 index 0000000000..4b81c7bf8e --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureLoadingContainer.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { createMessage, LOADING_SCHEMA } from "@appsmith/constants/messages"; +import { Spinner, Text } from "design-system"; +import styled from "styled-components"; + +const LoadingContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + & > p { + margin-left: 0.5rem; + } +`; + +const SpinnerWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +const DatasourceStructureLoadingContainer = () => { + return ( + + + + + + {createMessage(LOADING_SCHEMA)} + + + ); +}; + +export default DatasourceStructureLoadingContainer; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureNotFound.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureNotFound.tsx new file mode 100644 index 0000000000..607d456e42 --- /dev/null +++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureNotFound.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { useSelector } from "react-redux"; +import styled from "styled-components"; +import { Text, Button } from "design-system"; +import type { APIResponseError } from "api/ApiResponses"; +import { EDIT_DATASOURCE, createMessage } from "@appsmith/constants/messages"; +import AnalyticsUtil from "utils/AnalyticsUtil"; +import { DatasourceEditEntryPoints } from "constants/Datasource"; +import history from "utils/history"; +import { getQueryParams } from "utils/URLUtils"; +import { datasourcesEditorIdURL } from "RouteBuilder"; +import { omit } from "lodash"; +import { getCurrentPageId } from "selectors/editorSelectors"; + +export type Props = { + error: APIResponseError | { message: string } | undefined; + datasourceId: string; + pluginName?: string; +}; + +const NotFoundContainer = styled.div` + display: flex; + height: 100%; + width: 100%; + flex-direction: column; +`; + +const NotFoundText = styled(Text)` + margin-bottom: 1rem; + margin-top: 0.3rem; +`; + +const ButtonWrapper = styled.div` + width: fit-content; +`; + +const DatasourceStructureNotFound = (props: Props) => { + const { datasourceId, error, pluginName } = props; + + const pageId = useSelector(getCurrentPageId); + + const editDatasource = () => { + AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", { + datasourceId: datasourceId, + pluginName: pluginName, + entryPoint: DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA, + }); + + const url = datasourcesEditorIdURL({ + pageId, + datasourceId: datasourceId, + params: { ...omit(getQueryParams(), "viewMode"), viewMode: false }, + }); + history.push(url); + }; + + return ( + + {error?.message && ( + + {error.message} + + )} + + + + + ); +}; + +export default DatasourceStructureNotFound; diff --git a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx index 4f01fc0de8..1450313194 100644 --- a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx +++ b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from "react"; +import React, { useCallback, useContext } from "react"; import { useDispatch, useSelector } from "react-redux"; import { createActionRequest } from "actions/pluginActionActions"; import type { AppState } from "@appsmith/reducers"; @@ -11,25 +11,66 @@ import type { QueryAction } from "entities/Action"; import history from "utils/history"; import type { Datasource, QueryTemplate } from "entities/Datasource"; import { INTEGRATION_TABS } from "constants/routes"; -import { getDatasource, getPlugin } from "selectors/entitiesSelector"; +import { + getAction, + getDatasource, + getPlugin, +} from "selectors/entitiesSelector"; import { integrationEditorURL } from "RouteBuilder"; 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 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"; type QueryTemplatesProps = { templates: QueryTemplate[]; datasourceId: string; onSelect: () => void; + context: DatasourceStructureContext; + currentActionId: string; }; +enum QueryTemplatesEvent { + EXPLORER_TEMPLATE = "explorer-template", + QUERY_EDITOR_TEMPLATE = "query-editor-template", +} + +const TemplateMenuItem = styled(MenuItem)` + & > span { + text-transform: lowercase; + } + + & > span:first-letter { + text-transform: capitalize; + } +`; + export function QueryTemplates(props: QueryTemplatesProps) { const dispatch = useDispatch(); + const { isOpened: isWalkthroughOpened, popFeature } = + useContext(WalkthroughContext) || {}; const applicationId = useSelector(getCurrentApplicationId); const actions = useSelector((state: AppState) => state.entities.actions); const currentPageId = useSelector(getCurrentPageId); const dataSource: Datasource | undefined = useSelector((state: AppState) => getDatasource(state, props.datasourceId), ); + + const currentAction = useSelector((state) => + getAction(state, props.currentActionId), + ); + const formName = QUERY_EDITOR_FORM_NAME; + + const formValues = useSelector((state) => getFormValues(formName)(state)); + const plugin: Plugin | undefined = useSelector((state: AppState) => getPlugin(state, !!dataSource?.pluginId ? dataSource.pluginId : ""), ); @@ -55,14 +96,24 @@ export function QueryTemplates(props: QueryTemplatesProps) { }, eventData: { actionType: "Query", - from: "explorer-template", + from: + props?.context === DatasourceStructureContext.EXPLORER + ? QueryTemplatesEvent.EXPLORER_TEMPLATE + : QueryTemplatesEvent.QUERY_EDITOR_TEMPLATE, dataSource: dataSource?.name, datasourceId: props.datasourceId, pluginName: plugin?.name, + queryType: template.title, }, ...queryactionConfiguration, }), ); + + if (isWalkthroughOpened) { + popFeature && popFeature(); + setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true); + } + history.push( integrationEditorURL({ pageId: currentPageId, @@ -80,19 +131,84 @@ export function QueryTemplates(props: QueryTemplatesProps) { ], ); + const updateQueryAction = useCallback( + (template: QueryTemplate) => { + if (!currentAction) return; + + const queryactionConfiguration: Partial = { + actionConfiguration: { + body: template.body, + pluginSpecifiedTemplates: template.pluginSpecifiedTemplates, + formData: template.configuration, + ...template.actionConfiguration, + }, + }; + + const newFormValueState = { + ...formValues, + ...queryactionConfiguration, + }; + + const differences = diff(formValues, newFormValueState) || []; + + differences.forEach((diff) => { + if (diff.kind === "E" || diff.kind === "N") { + const path = diff?.path?.join(".") || ""; + const value = diff?.rhs; + + if (path) { + dispatch(change(QUERY_EDITOR_FORM_NAME, path, value)); + } + } + }); + + AnalyticsUtil.logEvent("AUTOMATIC_QUERY_GENERATION", { + datasourceId: props.datasourceId, + pluginName: plugin?.name || "", + templateCommand: template?.title, + isWalkthroughOpened, + }); + + if (isWalkthroughOpened) { + popFeature && popFeature(); + setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true); + } + + showUndoRedoToast( + currentAction.name, + false, + false, + true, + UndoRedoToastContext.QUERY_TEMPLATES, + ); + }, + [ + dispatch, + actions, + currentPageId, + applicationId, + props.datasourceId, + dataSource, + ], + ); + return ( <> {props.templates.map((template) => { return ( - { - createQueryAction(template); + if (props.currentActionId) { + updateQueryAction(template); + } else { + createQueryAction(template); + } props.onSelect(); }} > {template.title} - + ); })} diff --git a/app/client/src/pages/Editor/Explorer/Entity/index.tsx b/app/client/src/pages/Editor/Explorer/Entity/index.tsx index d302f48aca..4dced47790 100644 --- a/app/client/src/pages/Editor/Explorer/Entity/index.tsx +++ b/app/client/src/pages/Editor/Explorer/Entity/index.tsx @@ -255,7 +255,7 @@ export type EntityProps = { export const Entity = forwardRef( (props: EntityProps, ref: React.Ref) => { const isEntityOpen = useSelector((state: AppState) => - getEntityCollapsibleState(state, props.name), + getEntityCollapsibleState(state, props.entityId), ); const isDefaultExpanded = useMemo(() => !!props.isDefaultExpanded, []); const { canEditEntityName = false, showAddButton = false } = props; @@ -270,7 +270,7 @@ export const Entity = forwardRef( const open = (shouldOpen: boolean | undefined) => { if (!!props.children && props.name && isOpen !== shouldOpen) { - dispatch(setEntityCollapsibleState(props.name, !!shouldOpen)); + dispatch(setEntityCollapsibleState(props.entityId, !!shouldOpen)); } }; diff --git a/app/client/src/pages/Editor/Explorer/Files/index.tsx b/app/client/src/pages/Editor/Explorer/Files/index.tsx index 8128823b93..7199240b8a 100644 --- a/app/client/src/pages/Editor/Explorer/Files/index.tsx +++ b/app/client/src/pages/Editor/Explorer/Files/index.tsx @@ -118,13 +118,13 @@ function Files() { openMenu={isMenuOpen} /> } - entityId={pageId + "_widgets"} + entityId={pageId + "_actions"} icon={null} isDefaultExpanded={ - isFilesOpen === null || isFilesOpen === undefined ? false : isFilesOpen + isFilesOpen === null || isFilesOpen === undefined ? true : isFilesOpen } isSticky - key={pageId + "_widgets"} + key={pageId + "_actions"} name="Queries/JS" onCreate={onCreate} onToggle={onFilesToggle} diff --git a/app/client/src/pages/Editor/Explorer/Libraries/index.tsx b/app/client/src/pages/Editor/Explorer/Libraries/index.tsx index 4b2789da67..22828a03e8 100644 --- a/app/client/src/pages/Editor/Explorer/Libraries/index.tsx +++ b/app/client/src/pages/Editor/Explorer/Libraries/index.tsx @@ -22,7 +22,10 @@ import { } from "actions/JSLibraryActions"; import EntityAddButton from "../Entity/AddButton"; import type { TJSLibrary } from "workers/common/JSLibrary"; -import { getPagePermissions } from "selectors/editorSelectors"; +import { + getCurrentPageId, + getPagePermissions, +} from "selectors/editorSelectors"; import { hasCreateActionPermission } from "@appsmith/utils/permissionHelpers"; import recommendedLibraries from "./recommendedLibraries"; import { useTransition, animated } from "react-spring"; @@ -266,6 +269,7 @@ function LibraryEntity({ lib }: { lib: TJSLibrary }) { } function JSDependencies() { + const pageId = useSelector(getCurrentPageId) || ""; const libraries = useSelector(selectLibrariesForExplorer); const transitions = useTransition(libraries, { keys: (lib) => lib.name, @@ -311,7 +315,7 @@ function JSDependencies() { } - entityId="library_section" + entityId={pageId + "_library_section"} icon={null} isDefaultExpanded={isOpen} isSticky diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts index 364e92fe1d..3502f52e22 100644 --- a/app/client/src/pages/Editor/Explorer/hooks.ts +++ b/app/client/src/pages/Editor/Explorer/hooks.ts @@ -345,9 +345,8 @@ export const useFilteredEntities = ( }; export const useEntityUpdateState = (entityId: string) => { - return useSelector( - (state: AppState) => - get(state, "ui.explorer.entity.updatingEntity") === entityId, + return useSelector((state: AppState) => + get(state, "ui.explorer.entity.updatingEntity")?.includes(entityId), ); }; diff --git a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx index 8d5b8f44e4..6ee2bf26c4 100644 --- a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx +++ b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx @@ -231,10 +231,6 @@ function GeneratePageForm() { useState(""); const datasourcesStructure = useSelector(getDatasourcesStructure); - const isFetchingDatasourceStructure = useSelector( - getIsFetchingDatasourceStructure, - ); - const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = useSelector( getGenerateCRUDEnabledPluginMap, ); @@ -249,6 +245,10 @@ function GeneratePageForm() { DEFAULT_DROPDOWN_OPTION, ); + const isFetchingDatasourceStructure = useSelector((state: AppState) => + getIsFetchingDatasourceStructure(state, selectedDatasource.id || ""), + ); + const [isSelectedTableEmpty, setIsSelectedTableEmpty] = useState(false); diff --git a/app/client/src/pages/Editor/GuidedTour/utils.ts b/app/client/src/pages/Editor/GuidedTour/utils.ts index 5ce5fe4c06..685feb86d6 100644 --- a/app/client/src/pages/Editor/GuidedTour/utils.ts +++ b/app/client/src/pages/Editor/GuidedTour/utils.ts @@ -59,7 +59,12 @@ class IndicatorHelper { this.indicatorWidthOffset + "px"; } else if (position === "bottom") { - this.indicatorWrapper.style.top = coordinates.height + offset.top + "px"; + this.indicatorWrapper.style.top = + coordinates.top + + coordinates.height - + this.indicatorHeightOffset + + offset.top + + "px"; this.indicatorWrapper.style.left = coordinates.width / 2 + coordinates.left - @@ -68,7 +73,7 @@ class IndicatorHelper { "px"; } else if (position === "left") { this.indicatorWrapper.style.top = - coordinates.top + this.indicatorHeightOffset + offset.top + "px"; + coordinates.top - this.indicatorHeightOffset + offset.top + "px"; this.indicatorWrapper.style.left = coordinates.left - this.indicatorWidthOffset + offset.left + "px"; } else { @@ -90,6 +95,7 @@ class IndicatorHelper { offset: { top: number; left: number; + zIndex?: number; }, ) { if (this.timerId || this.indicatorWrapper) this.destroy(); @@ -111,6 +117,9 @@ class IndicatorHelper { loop: true, }); + if (offset.zIndex) { + this.indicatorWrapper.style.zIndex = `${offset.zIndex}`; + } // This is to invoke at the start and then recalculate every 3 seconds // 3 seconds is an arbitrary value here to avoid calling getBoundingClientRect to many times this.calculate(primaryReference, position, offset); @@ -237,7 +246,7 @@ export function highlightSection( export function showIndicator( selector: string, position = "right", - offset = { top: 0, left: 0 }, + offset: { top: number; left: number; zIndex?: number } = { top: 0, left: 0 }, ) { let primaryReference: Element | null = null; diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx index e27ccf6a6e..1516d1d0fb 100644 --- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx +++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx @@ -9,7 +9,12 @@ import { getPluginNameFromId, } from "selectors/entitiesSelector"; import FormControl from "../FormControl"; -import type { Action, QueryAction, SaaSAction } from "entities/Action"; +import { + PluginName, + type Action, + type QueryAction, + type SaaSAction, +} from "entities/Action"; import { useDispatch, useSelector } from "react-redux"; import ActionNameEditor from "components/editorComponents/ActionNameEditor"; import DropdownField from "components/editorComponents/form/fields/DropdownField"; @@ -126,6 +131,9 @@ import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole"; import { DocsLink, openDoc } from "../../../constants/DocumentationLinks"; import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView"; import { CloseDebugger } from "components/editorComponents/Debugger/DebuggerTabs"; +import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer"; +import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors"; +import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag"; const QueryFormContainer = styled.form` flex: 1; @@ -304,12 +312,12 @@ const DocumentationButton = styled(Button)` const SidebarWrapper = styled.div<{ show: boolean }>` border-left: 1px solid var(--ads-v2-color-border); - padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-7); - overflow: auto; + padding: 0 var(--ads-v2-spaces-4) var(--ads-v2-spaces-4); + overflow: hidden; border-bottom: 0; display: ${(props) => (props.show ? "flex" : "none")}; width: ${(props) => props.theme.actionSidePane.width}px; - margin-top: 38px; + margin-top: 10px; /* margin-left: var(--ads-v2-spaces-7); */ `; @@ -354,6 +362,7 @@ type QueryFormProps = { id, value, }: UpdateActionPropertyActionPayload) => void; + datasourceId: string; }; type ReduxProps = { @@ -870,6 +879,23 @@ export function EditorJSONtoForm(props: Props) { //TODO: move this to a common place const onClose = () => dispatch(showDebugger(false)); + // A/B feature flag for datasource structure. + const isEnabledForDSSchema = useSelector((state) => + selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_schema_enabled), + ); + + // A/B feature flag for query binding. + const isEnabledForQueryBinding = useSelector((state) => + selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled), + ); + + // here we check for normal conditions for opening action pane + // or if any of the flags are true, We should open the actionpane by default. + const shouldOpenActionPaneByDefault = + ((hasDependencies || !!output) && !guidedTourEnabled) || + ((isEnabledForDSSchema || isEnabledForQueryBinding) && + currentActionPluginName !== PluginName.SMTP); + // when switching between different redux forms, make sure this redux form has been initialized before rendering anything. // the initialized prop below comes from redux-form. if (!props.initialized) { @@ -1070,14 +1096,15 @@ export function EditorJSONtoForm(props: Props) { )}
- + diff --git a/app/client/src/pages/Editor/QueryEditor/index.tsx b/app/client/src/pages/Editor/QueryEditor/index.tsx index f6fb8d8e91..020276eede 100644 --- a/app/client/src/pages/Editor/QueryEditor/index.tsx +++ b/app/client/src/pages/Editor/QueryEditor/index.tsx @@ -252,6 +252,7 @@ class QueryEditor extends React.Component { return ( { onCreateDatasourceClick={this.onCreateDatasourceClick} onDeleteClick={this.handleDeleteClick} onRunClick={this.handleRunClick} + pluginId={this.props.pluginId} runErrorMessage={runErrorMessage[actionId]} settingConfig={settingConfig} uiComponent={uiComponent} diff --git a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx index 3482b3f17b..5b25436e52 100644 --- a/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx +++ b/app/client/src/pages/Editor/SaaSEditor/DatasourceForm.tsx @@ -437,6 +437,7 @@ class DatasourceSaaSEditor extends JSONtoForm { e.preventDefault(); }} showFilterComponent={false} + viewMode={viewMode} > {(!viewMode || createFlow || isInsideReconnectModal) && ( <> diff --git a/app/client/src/pages/Editor/utils.ts b/app/client/src/pages/Editor/utils.ts index 3d3ec92d56..c2a3d2b544 100644 --- a/app/client/src/pages/Editor/utils.ts +++ b/app/client/src/pages/Editor/utils.ts @@ -1,5 +1,5 @@ import { getDependenciesFromInverseDependencies } from "components/editorComponents/Debugger/helpers"; -import _, { debounce } from "lodash"; +import _, { debounce, random } from "lodash"; import { useEffect, useMemo, useState } from "react"; import ReactDOM from "react-dom"; import { useLocation } from "react-router"; @@ -296,3 +296,12 @@ export function useHref( return href; } + +// Ended up not using it, but leaving it here, incase anyone needs a helper function to generate random numbers. +export const generateRandomNumbers = ( + lowerBound = 1000, + upperBound = 9000, + allowFloating = false, +) => { + return random(lowerBound, upperBound, allowFloating); +}; diff --git a/app/client/src/pages/setup/SignupSuccess.tsx b/app/client/src/pages/setup/SignupSuccess.tsx index 3d49fe218b..1429a067ab 100644 --- a/app/client/src/pages/setup/SignupSuccess.tsx +++ b/app/client/src/pages/setup/SignupSuccess.tsx @@ -14,6 +14,7 @@ import { Center } from "pages/setup/common"; import { Spinner } from "design-system"; import { isValidLicense } from "@appsmith/selectors/tenantSelectors"; import { redirectUserAfterSignup } from "@appsmith/utils/signupHelpers"; +import { setUserSignedUpFlag } from "utils/storage"; export function SignupSuccess() { const dispatch = useDispatch(); @@ -23,8 +24,11 @@ export function SignupSuccess() { "enableFirstTimeUserExperience", ); const validLicense = useSelector(isValidLicense); + const user = useSelector(getCurrentUser); + useEffect(() => { PerformanceTracker.stopTracking(PerformanceTransactionName.SIGN_UP); + user?.email && setUserSignedUpFlag(user?.email); }, []); const redirectUsingQueryParam = useCallback( @@ -49,7 +53,6 @@ export function SignupSuccess() { redirectUsingQueryParam(); }, []); - const user = useSelector(getCurrentUser); const { cloudHosting } = getAppsmithConfigs(); const isCypressEnv = !!(window as any).Cypress; diff --git a/app/client/src/reducers/entityReducers/datasourceReducer.ts b/app/client/src/reducers/entityReducers/datasourceReducer.ts index a1ab5cb1a2..16aa383b7d 100644 --- a/app/client/src/reducers/entityReducers/datasourceReducer.ts +++ b/app/client/src/reducers/entityReducers/datasourceReducer.ts @@ -20,8 +20,7 @@ export interface DatasourceDataState { loading: boolean; isTesting: boolean; isListing: boolean; // fetching unconfigured datasource list - fetchingDatasourceStructure: boolean; - isRefreshingStructure: boolean; + fetchingDatasourceStructure: Record; structure: Record; isFetchingMockDataSource: false; mockDatasourceList: any[]; @@ -48,8 +47,7 @@ const initialState: DatasourceDataState = { loading: false, isTesting: false, isListing: false, - fetchingDatasourceStructure: false, - isRefreshingStructure: false, + fetchingDatasourceStructure: {}, structure: {}, isFetchingMockDataSource: false, mockDatasourceList: [], @@ -143,8 +141,15 @@ const datasourceReducer = createReducer(initialState, { }, [ReduxActionTypes.REFRESH_DATASOURCE_STRUCTURE_INIT]: ( state: DatasourceDataState, + action: ReduxAction<{ id: string }>, ) => { - return { ...state, isRefreshingStructure: true }; + return { + ...state, + fetchingDatasourceStructure: { + ...state.fetchingDatasourceStructure, + [action.payload.id]: true, + }, + }; }, [ReduxActionTypes.EXECUTE_DATASOURCE_QUERY_INIT]: ( state: DatasourceDataState, @@ -158,8 +163,15 @@ const datasourceReducer = createReducer(initialState, { }, [ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_INIT]: ( state: DatasourceDataState, + action: ReduxAction<{ id: string }>, ) => { - return { ...state, fetchingDatasourceStructure: true }; + return { + ...state, + fetchingDatasourceStructure: { + ...state.fetchingDatasourceStructure, + [action.payload.id]: true, + }, + }; }, [ReduxActionTypes.FETCH_DATASOURCE_STRUCTURE_SUCCESS]: ( state: DatasourceDataState, @@ -167,7 +179,10 @@ const datasourceReducer = createReducer(initialState, { ) => { return { ...state, - fetchingDatasourceStructure: false, + fetchingDatasourceStructure: { + ...state.fetchingDatasourceStructure, + [action.payload.datasourceId]: false, + }, structure: { ...state.structure, [action.payload.datasourceId]: action.payload.data, @@ -180,7 +195,10 @@ const datasourceReducer = createReducer(initialState, { ) => { return { ...state, - isRefreshingStructure: false, + fetchingDatasourceStructure: { + ...state.fetchingDatasourceStructure, + [action.payload.datasourceId]: false, + }, structure: { ...state.structure, [action.payload.datasourceId]: action.payload.data, @@ -189,10 +207,14 @@ const datasourceReducer = createReducer(initialState, { }, [ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR]: ( state: DatasourceDataState, + action: ReduxAction<{ datasourceId: string }>, ) => { return { ...state, - fetchingDatasourceStructure: false, + fetchingDatasourceStructure: { + ...state.fetchingDatasourceStructure, + [action.payload.datasourceId]: false, + }, }; }, [ReduxActionTypes.FETCH_DATASOURCES_SUCCESS]: ( @@ -464,10 +486,14 @@ const datasourceReducer = createReducer(initialState, { }, [ReduxActionErrorTypes.REFRESH_DATASOURCE_STRUCTURE_ERROR]: ( state: DatasourceDataState, + action: ReduxAction<{ datasourceId: string }>, ) => { return { ...state, - isRefreshingStructure: false, + fetchingDatasourceStructure: { + ...state.fetchingDatasourceStructure, + [action.payload.datasourceId]: false, + }, }; }, [ReduxActionErrorTypes.EXECUTE_DATASOURCE_QUERY_ERROR]: ( diff --git a/app/client/src/sagas/DatasourcesSagas.ts b/app/client/src/sagas/DatasourcesSagas.ts index 8b9a2000f9..f99460a63f 100644 --- a/app/client/src/sagas/DatasourcesSagas.ts +++ b/app/client/src/sagas/DatasourcesSagas.ts @@ -150,6 +150,7 @@ import { isGoogleSheetPluginDS, } from "utils/editorContextUtils"; import { getDefaultEnvId } from "@appsmith/api/ApiUtils"; +import type { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer"; function* fetchDatasourcesSaga( action: ReduxAction<{ workspaceId?: string } | undefined>, @@ -1173,17 +1174,19 @@ function* updateDatasourceSuccessSaga(action: UpdateDatasourceSuccessAction) { } function* fetchDatasourceStructureSaga( - action: ReduxAction<{ id: string; ignoreCache: boolean }>, + action: ReduxAction<{ + id: string; + ignoreCache: boolean; + schemaFetchContext: DatasourceStructureContext; + }>, ) { const datasource = shouldBeDefined( yield select(getDatasource, action.payload.id), `Datasource not found for id - ${action.payload.id}`, ); const plugin: Plugin = yield select(getPlugin, datasource?.pluginId); - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - }); + let errorMessage = ""; + let isSuccess = false; try { const response: ApiResponse = yield DatasourcesApi.fetchDatasourceStructure( @@ -1201,11 +1204,7 @@ function* fetchDatasourceStructureSaga( }); if (isEmpty(response.data)) { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - errorMessage: createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE), - }); + errorMessage = createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE); AppsmithConsole.warning({ text: "Datasource structure could not be retrieved", source: { @@ -1215,10 +1214,7 @@ function* fetchDatasourceStructureSaga( }, }); } else { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_SUCCESS", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - }); + isSuccess = true; AppsmithConsole.info({ text: "Datasource structure retrieved", source: { @@ -1229,25 +1225,17 @@ function* fetchDatasourceStructureSaga( }); } if (!!(response.data as any)?.error) { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - errorCode: (response.data as any).error?.code, - errorMessage: (response.data as any).error?.message, - }); + errorMessage = (response.data as any).error?.message; } } } catch (error) { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - errorMessage: (error as any)?.message, - }); + errorMessage = (error as any)?.message; yield put({ type: ReduxActionErrorTypes.FETCH_DATASOURCE_STRUCTURE_ERROR, payload: { error, show: false, + datasourceId: action.payload.id, }, }); AppsmithConsole.error({ @@ -1259,6 +1247,13 @@ function* fetchDatasourceStructureSaga( }, }); } + AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", { + datasourceId: datasource?.id, + pluginName: plugin?.name, + errorMessage: errorMessage, + isSuccess: isSuccess, + source: action.payload.schemaFetchContext, + }); } function* addAndFetchDatasourceStructureSaga( @@ -1291,16 +1286,19 @@ function* addAndFetchDatasourceStructureSaga( } } -function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) { +function* refreshDatasourceStructure( + action: ReduxAction<{ + id: string; + schemaRefreshContext: DatasourceStructureContext; + }>, +) { const datasource = shouldBeDefined( yield select(getDatasource, action.payload.id), `Datasource is not found for it - ${action.payload.id}`, ); const plugin: Plugin = yield select(getPlugin, datasource?.pluginId); - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - }); + let errorMessage = ""; + let isSuccess = false; try { const response: ApiResponse = yield DatasourcesApi.fetchDatasourceStructure( @@ -1318,11 +1316,7 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) { }); if (isEmpty(response.data)) { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - errorMessage: createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE), - }); + errorMessage = createMessage(DATASOURCE_SCHEMA_NOT_AVAILABLE); AppsmithConsole.warning({ text: "Datasource structure could not be retrieved", source: { @@ -1332,10 +1326,7 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) { }, }); } else { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_SUCCESS", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - }); + isSuccess = true; AppsmithConsole.info({ text: "Datasource structure retrieved", source: { @@ -1346,25 +1337,17 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) { }); } if (!!(response.data as any)?.error) { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - errorCode: (response.data as any).error?.code, - errorMessage: (response.data as any).error?.message, - }); + errorMessage = (response.data as any)?.message; } } } catch (error) { - AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH_FAILURE", { - datasourceId: datasource?.id, - pluginName: plugin?.name, - errorMessage: (error as any)?.message, - }); + errorMessage = (error as any)?.message; yield put({ type: ReduxActionErrorTypes.REFRESH_DATASOURCE_STRUCTURE_ERROR, payload: { error, show: false, + datasourceId: action.payload.id, }, }); AppsmithConsole.error({ @@ -1376,6 +1359,14 @@ function* refreshDatasourceStructure(action: ReduxAction<{ id: string }>) { }, }); } + + AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_FETCH", { + datasourceId: datasource?.id, + pluginName: plugin?.name, + errorMessage: errorMessage, + isSuccess: isSuccess, + source: action.payload.schemaRefreshContext, + }); } function* executeDatasourceQuerySaga( diff --git a/app/client/src/sagas/ReplaySaga.ts b/app/client/src/sagas/ReplaySaga.ts index e723ad5fa9..bdad4bbe48 100644 --- a/app/client/src/sagas/ReplaySaga.ts +++ b/app/client/src/sagas/ReplaySaga.ts @@ -46,8 +46,10 @@ import { } from "./EvaluationsSaga"; import { createBrowserHistory } from "history"; import { + getDatasource, getEditorConfig, getPluginForm, + getPlugins, getSettingConfig, } from "selectors/entitiesSelector"; import type { Action } from "entities/Action"; @@ -73,6 +75,11 @@ import { import { AppThemingMode } from "selectors/appThemingSelectors"; import { generateAutoHeightLayoutTreeAction } from "actions/autoHeightActions"; import { SelectionRequestType } from "sagas/WidgetSelectUtils"; +import { startFormEvaluations } from "actions/evaluationActions"; +import { getCurrentEnvironment } from "@appsmith/utils/Environments"; +import { getUIComponent } from "pages/Editor/QueryEditor/helpers"; +import type { Plugin } from "api/PluginApi"; +import { UIComponentTypes } from "api/PluginApi"; export type UndoRedoPayload = { operation: ReplayReduxActionTypes; @@ -309,18 +316,49 @@ function* replayActionSaga( /** * Update all the diffs in the action object. * We need this for debugger logs, dynamicBindingPathList and to call relevant APIs */ + + const currentEnvironment = getCurrentEnvironment(); + const plugins: Plugin[] = yield select(getPlugins); + const uiComponent = getUIComponent(replayEntity.pluginId, plugins); + const datasource: Datasource | undefined = yield select( + getDatasource, + replayEntity.datasource?.id || "", + ); + yield all( - updates.map((u) => - put( - setActionProperty({ - actionId: replayEntity.id, - propertyName: u.modifiedProperty, - value: - u.kind === "A" ? _.get(replayEntity, u.modifiedProperty) : u.update, - skipSave: true, - }), - ), - ), + updates.map((u) => { + // handle evaluations after update. + const postEvalActions = + uiComponent === UIComponentTypes.UQIDbEditorForm + ? [ + startFormEvaluations( + replayEntity.id, + replayEntity.actionConfiguration, + replayEntity.datasource.id || "", + replayEntity.pluginId, + u.modifiedProperty, + true, + datasource?.datasourceStorages[currentEnvironment] + .datasourceConfiguration, + ), + ] + : []; + + return put( + setActionProperty( + { + actionId: replayEntity.id, + propertyName: u.modifiedProperty, + value: + u.kind === "A" + ? _.get(replayEntity, u.modifiedProperty) + : u.update, + skipSave: true, + }, + postEvalActions, + ), + ); + }), ); //Save the updated action object diff --git a/app/client/src/sagas/SnipingModeSagas.ts b/app/client/src/sagas/SnipingModeSagas.ts index 00d79ea7c7..a79f3e3ba7 100644 --- a/app/client/src/sagas/SnipingModeSagas.ts +++ b/app/client/src/sagas/SnipingModeSagas.ts @@ -21,6 +21,11 @@ import { setSnipingMode } from "actions/propertyPaneActions"; import { selectWidgetInitAction } from "actions/widgetSelectionActions"; import { SelectionRequestType } from "sagas/WidgetSelectUtils"; import { toast } from "design-system"; +import { + AB_TESTING_EVENT_KEYS, + FEATURE_FLAG, +} from "@appsmith/entities/FeatureFlag"; +import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors"; const WidgetTypes = WidgetFactory.widgetTypes; @@ -36,6 +41,10 @@ export function* bindDataToWidgetSaga( ), ); const widgetState: CanvasWidgetsReduxState = yield select(getCanvasWidgets); + const isDSBindingEnabled: boolean = yield select( + selectFeatureFlagCheck, + FEATURE_FLAG.ab_ds_binding_enabled, + ); const selectedWidget = widgetState[action.payload.widgetId]; if (!selectedWidget || !selectedWidget.type) { @@ -149,6 +158,9 @@ export function* bindDataToWidgetSaga( apiId: queryId, propertyPath, propertyValue, + [AB_TESTING_EVENT_KEYS.abTestingFlagLabel]: + FEATURE_FLAG.ab_ds_binding_enabled, + [AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isDSBindingEnabled, }); if (queryId && isValidProperty) { // set the property path to dynamic, i.e. enable JS mode diff --git a/app/client/src/selectors/entitiesSelector.ts b/app/client/src/selectors/entitiesSelector.ts index 122822646e..c05dfe54e0 100644 --- a/app/client/src/selectors/entitiesSelector.ts +++ b/app/client/src/selectors/entitiesSelector.ts @@ -117,8 +117,11 @@ export const getDatasourceFirstTableName = ( return ""; }; -export const getIsFetchingDatasourceStructure = (state: AppState): boolean => { - return state.entities.datasources.fetchingDatasourceStructure; +export const getIsFetchingDatasourceStructure = ( + state: AppState, + datasourceId: string, +): boolean => { + return state.entities.datasources.fetchingDatasourceStructure[datasourceId]; }; export const getMockDatasources = (state: AppState): MockDatasource[] => { @@ -222,6 +225,30 @@ export const getPluginNameFromId = ( return plugin.name; }; +export const getPluginPackageNameFromId = ( + state: AppState, + pluginId: string, +): string => { + const plugin = state.entities.plugins.list.find( + (plugin) => plugin.id === pluginId, + ); + + if (!plugin) return ""; + return plugin.packageName; +}; + +export const getPluginDatasourceComponentFromId = ( + state: AppState, + pluginId: string, +): string => { + const plugin = state.entities.plugins.list.find( + (plugin) => plugin.id === pluginId, + ); + + if (!plugin) return ""; + return plugin.datasourceComponent; +}; + export const getPluginTypeFromDatasourceId = ( state: AppState, datasourceId: string, diff --git a/app/client/src/selectors/featureFlagsSelectors.ts b/app/client/src/selectors/featureFlagsSelectors.ts index 00f78b647d..004a15a8c0 100644 --- a/app/client/src/selectors/featureFlagsSelectors.ts +++ b/app/client/src/selectors/featureFlagsSelectors.ts @@ -1,14 +1,17 @@ import type { AppState } from "@appsmith/reducers"; -import { useSelector } from "react-redux"; import type { FeatureFlag } from "@appsmith/entities/FeatureFlag"; export const selectFeatureFlags = (state: AppState) => state.ui.users.featureFlag.data; -export function useFeatureFlagCheck(flagName: FeatureFlag): boolean { - const flagValues = useSelector(selectFeatureFlags); +// React hooks should not be placed in a selectors file. +export const selectFeatureFlagCheck = ( + state: AppState, + flagName: FeatureFlag, +): boolean => { + const flagValues = selectFeatureFlags(state); if (flagName in flagValues) { return flagValues[flagName]; } return false; -} +}; diff --git a/app/client/src/utils/AnalyticsUtil.tsx b/app/client/src/utils/AnalyticsUtil.tsx index 82d28217ef..7c2adaadc3 100644 --- a/app/client/src/utils/AnalyticsUtil.tsx +++ b/app/client/src/utils/AnalyticsUtil.tsx @@ -310,8 +310,6 @@ export type EventName = | "DISCARD_DATASOURCE_CHANGES" | "TEST_DATA_SOURCE_FAILED" | "DATASOURCE_SCHEMA_FETCH" - | "DATASOURCE_SCHEMA_FETCH_SUCCESS" - | "DATASOURCE_SCHEMA_FETCH_FAILURE" | "EDIT_ACTION_CLICK" | "QUERY_TEMPLATE_SELECTED" | "RUN_API_FAILURE" @@ -335,7 +333,16 @@ export type EventName = | "JS_VARIABLE_MUTATED" | "EXPLORER_WIDGET_CLICK" | "WIDGET_SEARCH" - | "MAKE_APPLICATION_PUBLIC"; + | "MAKE_APPLICATION_PUBLIC" + | WALKTHROUGH_EVENTS + | DATASOURCE_SCHEMA_EVENTS; + +export type DATASOURCE_SCHEMA_EVENTS = + | "DATASOURCE_SCHEMA_SEARCH" + | "DATASOURCE_SCHEMA_TABLE_SELECT" + | "AUTOMATIC_QUERY_GENERATION"; + +type WALKTHROUGH_EVENTS = "WALKTHROUGH_DISMISSED" | "WALKTHROUGH_SHOWN"; export type AI_EVENTS = | "AI_QUERY_SENT" diff --git a/app/client/src/utils/replayHelpers.tsx b/app/client/src/utils/replayHelpers.tsx index 00ef26b262..8f6043db06 100644 --- a/app/client/src/utils/replayHelpers.tsx +++ b/app/client/src/utils/replayHelpers.tsx @@ -13,6 +13,7 @@ import { BULK_WIDGET_ADDED, WIDGET_REMOVED, BULK_WIDGET_REMOVED, + ACTION_CONFIGURATION_CHANGED, } from "@appsmith/constants/messages"; import { toast } from "design-system"; import { setApiPaneConfigSelectedTabIndex } from "../actions/apiPaneActions"; @@ -47,25 +48,51 @@ export const processUndoRedoToasts = ( showUndoRedoToast(widgetName, isMultipleToasts, isCreated, !isUndo); }; +// context can be extended. +export enum UndoRedoToastContext { + WIDGET = "widget", + QUERY_TEMPLATES = "query-templates", +} + /** * shows a toast for undo/redo * - * @param widgetName + * @param actionName * @param isMultiple * @param isCreated * @param shouldUndo + * @param toastContext * @returns */ export const showUndoRedoToast = ( - widgetName: string | undefined, + actionName: string | undefined, isMultiple: boolean, isCreated: boolean, shouldUndo: boolean, + toastContext = UndoRedoToastContext.WIDGET, ) => { - if (shouldDisallowToast(shouldUndo)) return; + if ( + shouldDisallowToast(shouldUndo) && + toastContext === UndoRedoToastContext.WIDGET + ) + return; + + let actionDescription; + let actionText = ""; + + switch (toastContext) { + case UndoRedoToastContext.WIDGET: + actionDescription = getWidgetDescription(isCreated, isMultiple); + actionText = createMessage(actionDescription, actionName); + break; + case UndoRedoToastContext.QUERY_TEMPLATES: + actionDescription = ACTION_CONFIGURATION_CHANGED; + actionText = createMessage(actionDescription, actionName); + break; + default: + actionText = ""; + } - const actionDescription = getActionDescription(isCreated, isMultiple); - const widgetText = createMessage(actionDescription, widgetName); const action = shouldUndo ? "undo" : "redo"; const actionKey = shouldUndo ? `${modText()} Z` @@ -73,10 +100,10 @@ export const showUndoRedoToast = ( ? `${modText()} ${shiftText()} Z` : `${modText()} Y`; - toast.show(`${widgetText}. Press ${actionKey} to ${action}`); + toast.show(`${actionText}. Press ${actionKey} to ${action}`); }; -function getActionDescription(isCreated: boolean, isMultiple: boolean) { +function getWidgetDescription(isCreated: boolean, isMultiple: boolean) { if (isCreated) return isMultiple ? BULK_WIDGET_ADDED : WIDGET_ADDED; else return isMultiple ? BULK_WIDGET_REMOVED : WIDGET_REMOVED; } diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts index 8ae2aa402c..b53e7c5b36 100644 --- a/app/client/src/utils/storage.ts +++ b/app/client/src/utils/storage.ts @@ -23,6 +23,8 @@ export const STORAGE_KEYS: { FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY: "FIRST_TIME_USER_ONBOARDING_TELEMETRY_CALLOUT_VISIBILITY", SIGNPOSTING_APP_STATE: "SIGNPOSTING_APP_STATE", + FEATURE_WALKTHROUGH: "FEATURE_WALKTHROUGH", + USER_SIGN_UP: "USER_SIGN_UP", }; const store = localforage.createInstance({ @@ -418,3 +420,77 @@ export const setFirstTimeUserOnboardingTelemetryCalloutVisibility = async ( log.error(error); } }; + +export const setFeatureFlagShownStatus = async (key: string, value: any) => { + try { + let flagsJSON: Record | null = await store.getItem( + STORAGE_KEYS.FEATURE_WALKTHROUGH, + ); + + if (typeof flagsJSON === "object" && flagsJSON) { + flagsJSON[key] = value; + } else { + flagsJSON = { [key]: value }; + } + + await store.setItem(STORAGE_KEYS.FEATURE_WALKTHROUGH, flagsJSON); + return true; + } catch (error) { + log.error("An error occurred while updating FEATURE_WALKTHROUGH"); + log.error(error); + } +}; + +export const getFeatureFlagShownStatus = async (key: string) => { + try { + const flagsJSON: Record | null = await store.getItem( + STORAGE_KEYS.FEATURE_WALKTHROUGH, + ); + + if (typeof flagsJSON === "object" && flagsJSON) { + return !!flagsJSON[key]; + } + + return false; + } catch (error) { + log.error("An error occurred while reading FEATURE_WALKTHROUGH"); + log.error(error); + } +}; + +export const setUserSignedUpFlag = async (email: string) => { + try { + let userSignedUp: Record | null = await store.getItem( + STORAGE_KEYS.USER_SIGN_UP, + ); + + if (typeof userSignedUp === "object" && userSignedUp) { + userSignedUp[email] = Date.now(); + } else { + userSignedUp = { [email]: Date.now() }; + } + + await store.setItem(STORAGE_KEYS.USER_SIGN_UP, userSignedUp); + return true; + } catch (error) { + log.error("An error occurred while updating USER_SIGN_UP"); + log.error(error); + } +}; + +export const isUserSignedUpFlagSet = async (email: string) => { + try { + const userSignedUp: Record | null = await store.getItem( + STORAGE_KEYS.USER_SIGN_UP, + ); + + if (typeof userSignedUp === "object" && userSignedUp) { + return !!userSignedUp[email]; + } + + return false; + } catch (error) { + log.error("An error occurred while reading USER_SIGN_UP"); + log.error(error); + } +}; diff --git a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java index 379631c836..f1c136cc40 100644 --- a/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java +++ b/app/server/appsmith-interfaces/src/main/java/com/appsmith/external/constants/AnalyticsEvents.java @@ -79,8 +79,6 @@ public enum AnalyticsEvents { UPDATE_EXISTING_LICENSE("Update_Existing_License"), DS_SCHEMA_FETCH_EVENT("Datasource_Schema_Fetch"), - DS_SCHEMA_FETCH_EVENT_SUCCESS("Datasource_Schema_Fetch_Success"), - DS_SCHEMA_FETCH_EVENT_FAILED("Datasource_Schema_Fetch_Failed"), DS_TEST_EVENT("Test_Datasource_Clicked"), DS_TEST_EVENT_SUCCESS("Test_Datasource_Success"), diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/WidgetSuggestionHelper.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/WidgetSuggestionHelper.java index 41ce172350..1080ad708d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/WidgetSuggestionHelper.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/WidgetSuggestionHelper.java @@ -43,8 +43,6 @@ public class WidgetSuggestionHelper { widgetTypeList = handleJsonNode((JsonNode) data); } else if (data instanceof List && !((List) data).isEmpty()) { widgetTypeList = handleList((List) data); - } else if (data != null) { - widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET)); } return widgetTypeList; } @@ -93,13 +91,9 @@ public class WidgetSuggestionHelper { * Get fields from nested object * use the for table, list, chart and Select */ - if (dataFields.objectFields.isEmpty()) { - widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET)); - } else { + if (!dataFields.objectFields.isEmpty()) { String nestedFieldName = dataFields.getObjectFields().get(0); - if (node.get(nestedFieldName).size() == 0) { - widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET)); - } else { + if (node.get(nestedFieldName).size() != 0) { dataFields = collectFieldsFromData( node.get(nestedFieldName).get(0).fields()); widgetTypeList = getWidgetsForTypeNestedObject( @@ -134,7 +128,7 @@ public class WidgetSuggestionHelper { } return getWidgetsForTypeArray(fields, numericFields); } - return List.of(getWidget(WidgetType.TABLE_WIDGET_V2), getWidget(WidgetType.TEXT_WIDGET)); + return List.of(getWidget(WidgetType.TABLE_WIDGET_V2)); } /* @@ -170,7 +164,6 @@ public class WidgetSuggestionHelper { if (length > 1 && !fields.isEmpty()) { widgetTypeList.add(getWidget(WidgetType.SELECT_WIDGET, fields.get(0), fields.get(0))); } else { - widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET)); widgetTypeList.add(getWidget(WidgetType.INPUT_WIDGET)); } return widgetTypeList; @@ -178,6 +171,7 @@ public class WidgetSuggestionHelper { private static List getWidgetsForTypeArray(List fields, List numericFields) { List widgetTypeList = new ArrayList<>(); + widgetTypeList.add(getWidget(WidgetType.TABLE_WIDGET_V2)); if (!fields.isEmpty()) { if (fields.size() < 2) { widgetTypeList.add(getWidget(WidgetType.SELECT_WIDGET, fields.get(0), fields.get(0))); @@ -188,14 +182,11 @@ public class WidgetSuggestionHelper { widgetTypeList.add(getWidget(WidgetType.CHART_WIDGET, fields.get(0), numericFields.get(0))); } } - widgetTypeList.add(getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET)); return widgetTypeList; } private static List getWidgetsForTypeNumber() { List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(getWidget(WidgetType.TEXT_WIDGET)); widgetTypeList.add(getWidget(WidgetType.INPUT_WIDGET)); return widgetTypeList; } @@ -213,6 +204,7 @@ public class WidgetSuggestionHelper { * For the CHART widget we need at least one field of type int and one string type field * For the DROP_DOWN at least one String type field * */ + widgetTypeList.add(getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, nestedFieldName)); if (!fields.isEmpty()) { if (fields.size() < 2) { widgetTypeList.add( @@ -226,8 +218,6 @@ public class WidgetSuggestionHelper { WidgetType.CHART_WIDGET, nestedFieldName, fields.get(0), numericFields.get(0))); } } - widgetTypeList.add(getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, nestedFieldName)); - widgetTypeList.add(getWidgetNestedData(WidgetType.TEXT_WIDGET, nestedFieldName)); return widgetTypeList; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java index ad69135f1a..317653ea6a 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java @@ -360,9 +360,7 @@ public class AnalyticsServiceCEImpl implements AnalyticsServiceCE { AnalyticsEvents.DS_TEST_EVENT, AnalyticsEvents.DS_TEST_EVENT_SUCCESS, AnalyticsEvents.DS_TEST_EVENT_FAILED, - AnalyticsEvents.DS_SCHEMA_FETCH_EVENT, - AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_SUCCESS, - AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_FAILED); + AnalyticsEvents.DS_SCHEMA_FETCH_EVENT); } public Mono sendCreateEvent(T object, Map extraProperties) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/DatasourceStructureSolutionCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/DatasourceStructureSolutionCEImpl.java index f8c87c458a..5fdc7f1851 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/DatasourceStructureSolutionCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/DatasourceStructureSolutionCEImpl.java @@ -78,7 +78,7 @@ public class DatasourceStructureSolutionCEImpl implements DatasourceStructureSol if (Boolean.FALSE.equals(datasourceStorage.getIsValid())) { return analyticsService .sendObjectEvent( - AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_FAILED, + AnalyticsEvents.DS_SCHEMA_FETCH_EVENT, datasourceStorage, getAnalyticsPropertiesForTestEventStatus(datasourceStorage, false)) .then(Mono.just(new DatasourceStructure())); @@ -128,7 +128,7 @@ public class DatasourceStructureSolutionCEImpl implements DatasourceStructureSol }) .onErrorResume(error -> analyticsService .sendObjectEvent( - AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_FAILED, + AnalyticsEvents.DS_SCHEMA_FETCH_EVENT, datasourceStorage, getAnalyticsPropertiesForTestEventStatus(datasourceStorage, false, error)) .then(Mono.error(error))) @@ -138,7 +138,7 @@ public class DatasourceStructureSolutionCEImpl implements DatasourceStructureSol return analyticsService .sendObjectEvent( - AnalyticsEvents.DS_SCHEMA_FETCH_EVENT_SUCCESS, + AnalyticsEvents.DS_SCHEMA_FETCH_EVENT, datasourceStorage, getAnalyticsPropertiesForTestEventStatus(datasourceStorage, true, null)) .then( diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java index 232f7bc76f..b78c94248a 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/solutions/ce/ActionExecutionSolutionCETest.java @@ -369,7 +369,6 @@ public class ActionExecutionSolutionCETest { mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -405,7 +404,6 @@ public class ActionExecutionSolutionCETest { mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -438,7 +436,6 @@ public class ActionExecutionSolutionCETest { mockResult.setBody("response-body"); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -743,7 +740,6 @@ public class ActionExecutionSolutionCETest { mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -792,7 +788,6 @@ public class ActionExecutionSolutionCETest { mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value"))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -839,7 +834,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -943,10 +937,9 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "x", "y")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "x", "x")); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "x", "x")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "x", "y")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1054,10 +1047,9 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "id", "ppu")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "id", "type")); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "id", "type")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "id", "ppu")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1157,10 +1149,9 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1216,9 +1207,8 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "CarType", "carID")); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "CarType", "carID")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1258,7 +1248,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1301,7 +1290,6 @@ public class ActionExecutionSolutionCETest { List widgetTypeList = new ArrayList<>(); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1381,7 +1369,6 @@ public class ActionExecutionSolutionCETest { List widgetTypeList = new ArrayList<>(); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.INPUT_WIDGET)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1448,11 +1435,10 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users")); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.CHART_WIDGET, "users", "name", "id")); widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users")); widgetTypeList.add( WidgetSuggestionHelper.getWidgetNestedData(WidgetType.SELECT_WIDGET, "users", "name", "status")); + widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.CHART_WIDGET, "users", "name", "id")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1501,7 +1487,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1561,7 +1546,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1605,7 +1589,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, "users")); widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TABLE_WIDGET_V2, "users")); mockResult.setSuggestedWidgets(widgetTypeList); @@ -1650,7 +1633,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidgetNestedData(WidgetType.TEXT_WIDGET, null)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1695,10 +1677,9 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "url", "url")); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.CHART_WIDGET, "url", "width")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1745,9 +1726,8 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "width", "url")); widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TABLE_WIDGET_V2)); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); + widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.SELECT_WIDGET, "width", "url")); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1784,7 +1764,6 @@ public class ActionExecutionSolutionCETest { mockResult.setDataTypes(List.of(new ParsedDataType(DisplayDataType.RAW))); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO(); @@ -1830,7 +1809,6 @@ public class ActionExecutionSolutionCETest { .block(); List widgetTypeList = new ArrayList<>(); - widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET)); mockResult.setSuggestedWidgets(widgetTypeList); ActionDTO action = new ActionDTO();