feat: activation phase 1 (#25126)
Feature implementations: - Schema in the Api Right Side Pane; - New Bindings UI, which is now a suggested widget; - Feature walkthrough for the aforementioned two units only if you are a new user. Only those users who have the flags `ab_ds_binding_enabled` and `ab_ds_schema_enabled` independently set to true can see the implementation described above. https://www.notion.so/appsmith/Activation-60c64894f42d4cdcb92220c1dbc73802
This commit is contained in:
parent
daece53b66
commit
0dcef48dc8
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: {
|
|||
<ErrorPage code={props.safeCrashCode} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Walkthrough>
|
||||
<AppHeader />
|
||||
<Routes />
|
||||
</>
|
||||
</Walkthrough>
|
||||
)}
|
||||
</Suspense>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, WidgetBindingInfo> = {
|
||||
|
|
@ -57,42 +169,56 @@ export const WIDGET_DATA_FIELD_MAP: Record<string, WidgetBindingInfo> = {
|
|||
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 (
|
||||
<HeadingWrapper>
|
||||
<Text kind="heading-xs">{heading}</Text>
|
||||
<Text kind="body-s">{subHeading}</Text>
|
||||
</HeadingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function renderWidgetItem(
|
||||
icon: string | undefined,
|
||||
name: string | undefined,
|
||||
textKind: TextKind,
|
||||
) {
|
||||
return (
|
||||
<ItemWrapper>
|
||||
{icon && <img alt="widget-icon" src={icon} />}
|
||||
<Text className="widget-name" kind={textKind}>
|
||||
{name}
|
||||
</Text>
|
||||
</ItemWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function renderWidgetImage(image: string | undefined) {
|
||||
if (!!image) {
|
||||
return <img alt="widget-info-image" src={getAssetUrl(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 (
|
||||
<Collapsible label={label}>
|
||||
<WidgetList>
|
||||
{props.suggestedWidgets.map((suggestedWidget) => {
|
||||
const widgetInfo: WidgetBindingInfo | undefined =
|
||||
WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
|
||||
<SuggestedWidgetContainer id={BINDING_SECTION_ID}>
|
||||
{!!isEnabledForQueryBinding ? (
|
||||
<Collapsible label={labelNew}>
|
||||
{isTableWidgetPresentOnCanvas() && (
|
||||
<SubSection>
|
||||
{renderHeading(
|
||||
connectExistingWidgetLabel,
|
||||
connectExistingWidgetSubLabel,
|
||||
)}
|
||||
{!isWidgetsPresentOnCanvas && (
|
||||
<Text kind="body-s">{createMessage(NO_EXISTING_WIDGETS)}</Text>
|
||||
)}
|
||||
|
||||
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 */}
|
||||
{
|
||||
<ExistingWidgetList>
|
||||
{Object.keys(canvasWidgets).map((widgetKey) => {
|
||||
const widget: FlattenedWidgetProps | undefined =
|
||||
canvasWidgets[widgetKey];
|
||||
const widgetInfo: WidgetBindingInfo | undefined =
|
||||
WIDGET_DATA_FIELD_MAP[widget.type];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`widget t--suggested-widget-${suggestedWidget.type}`}
|
||||
key={suggestedWidget.type}
|
||||
onClick={() => addWidget(suggestedWidget, widgetInfo)}
|
||||
>
|
||||
<Tooltip content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}>
|
||||
<div className="image-wrapper">
|
||||
{widgetInfo.image && (
|
||||
<img
|
||||
alt="widget-info-image"
|
||||
src={getAssetUrl(widgetInfo.image)}
|
||||
/>
|
||||
)}
|
||||
if (!widgetInfo || widget?.type !== "TABLE_WIDGET_V2")
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`widget t--suggested-widget-${widget.type}`}
|
||||
key={widget.type + widget.widgetId}
|
||||
onClick={() => handleBindData(widgetKey)}
|
||||
>
|
||||
<Tooltip
|
||||
content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}
|
||||
>
|
||||
<div className="image-wrapper">
|
||||
{renderWidgetImage(widgetInfo.existingImage)}
|
||||
{renderWidgetItem(
|
||||
widgetInfo.icon,
|
||||
widget.widgetName,
|
||||
"body-s",
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ExistingWidgetList>
|
||||
}
|
||||
</SubSection>
|
||||
)}
|
||||
<SubSection>
|
||||
{renderHeading(addNewWidgetLabel, addNewWidgetSubLabel)}
|
||||
<WidgetList className="spacing">
|
||||
{props.suggestedWidgets.map((suggestedWidget) => {
|
||||
const widgetInfo: WidgetBindingInfo | undefined =
|
||||
WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
|
||||
|
||||
if (!widgetInfo) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`widget t--suggested-widget-${suggestedWidget.type}`}
|
||||
key={suggestedWidget.type}
|
||||
onClick={() => addWidget(suggestedWidget, widgetInfo)}
|
||||
>
|
||||
<Tooltip content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}>
|
||||
{renderWidgetItem(
|
||||
widgetInfo.icon,
|
||||
widgetInfo.widgetName,
|
||||
"body-m",
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</WidgetList>
|
||||
</SubSection>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<Collapsible label={labelOld}>
|
||||
<WidgetList>
|
||||
{props.suggestedWidgets.map((suggestedWidget) => {
|
||||
const widgetInfo: WidgetBindingInfo | undefined =
|
||||
WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
|
||||
|
||||
if (!widgetInfo) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`widget t--suggested-widget-${suggestedWidget.type}`}
|
||||
key={suggestedWidget.type}
|
||||
onClick={() => addWidget(suggestedWidget, widgetInfo)}
|
||||
>
|
||||
<Tooltip content={createMessage(SUGGESTED_WIDGET_TOOLTIP)}>
|
||||
<div className="image-wrapper">
|
||||
{renderWidgetImage(widgetInfo.image)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</WidgetList>
|
||||
</Collapsible>
|
||||
);
|
||||
})}
|
||||
</WidgetList>
|
||||
</Collapsible>
|
||||
)}
|
||||
</SuggestedWidgetContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<CollapsibleWrapper isOpen={isOpen}>
|
||||
<Label className="icon-text" onClick={() => setIsOpen(!isOpen)}>
|
||||
<Icon name="down-arrow" size="lg" />
|
||||
<span className="label">{label}</span>
|
||||
<Icon name={isOpen ? "down-arrow" : "arrow-right-s-line"} size="lg" />
|
||||
{!!customLabelComponent ? (
|
||||
customLabelComponent
|
||||
) : (
|
||||
<Text className="label" kind="heading-xs">
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</Label>
|
||||
<Collapse isOpen={isOpen} keepChildrenMounted>
|
||||
{children}
|
||||
|
|
@ -172,6 +242,24 @@ export function Collapsible({
|
|||
);
|
||||
}
|
||||
|
||||
export function DisabledCollapsible({
|
||||
label,
|
||||
tooltipLabel = "",
|
||||
}: DisabledCollapsibleProps) {
|
||||
return (
|
||||
<Tooltip content={tooltipLabel}>
|
||||
<CollapsibleWrapper isDisabled isOpen={false}>
|
||||
<Label className="icon-text">
|
||||
<Icon name="arrow-right-s-line" size="lg" />
|
||||
<Text className="label" kind="heading-xs">
|
||||
{label}
|
||||
</Text>
|
||||
</Label>
|
||||
</CollapsibleWrapper>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
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 <Placeholder>{createMessage(NO_CONNECTIONS)}</Placeholder>;
|
||||
}
|
||||
|
||||
|
|
@ -257,33 +453,69 @@ function ActionSidebar({
|
|||
{createMessage(BACK_TO_CANVAS)}
|
||||
</BackToCanvasLink>
|
||||
|
||||
{hasConnections && (
|
||||
{showSchema && (
|
||||
<SchemaSideBarSection height={50} id={SCHEMA_SECTION_ID}>
|
||||
<Collapsible
|
||||
customLabelComponent={
|
||||
<DatasourceStructureHeader datasourceId={datasourceId || ""} />
|
||||
}
|
||||
expand={!showSuggestedWidgets}
|
||||
label="Schema"
|
||||
>
|
||||
<DataStructureListWrapper>
|
||||
<DataStructureList
|
||||
context={context}
|
||||
currentActionId={params?.queryId || ""}
|
||||
datasourceId={datasourceId || ""}
|
||||
datasourceStructure={datasourceStructure}
|
||||
pluginName={pluginName}
|
||||
step={0}
|
||||
/>
|
||||
</DataStructureListWrapper>
|
||||
</Collapsible>
|
||||
</SchemaSideBarSection>
|
||||
)}
|
||||
|
||||
{showSchema && isEnabledForQueryBinding && <Divider />}
|
||||
|
||||
{hasConnections && !isEnabledForQueryBinding && (
|
||||
<Connections
|
||||
actionName={actionName}
|
||||
entityDependencies={entityDependencies}
|
||||
/>
|
||||
)}
|
||||
{canEditPage && hasResponse && Object.keys(widgets).length > 1 && (
|
||||
<Collapsible label="Connect widget">
|
||||
{/*<div className="description">Go to canvas and select widgets</div>*/}
|
||||
<SnipingWrapper>
|
||||
<Button
|
||||
className={"t--select-in-canvas"}
|
||||
kind="secondary"
|
||||
onClick={handleBindData}
|
||||
size="md"
|
||||
>
|
||||
Select widget
|
||||
</Button>
|
||||
</SnipingWrapper>
|
||||
</Collapsible>
|
||||
)}
|
||||
{showSuggestedWidgets && (
|
||||
<SuggestedWidgets
|
||||
actionName={actionName}
|
||||
hasWidgets={hasWidgets}
|
||||
suggestedWidgets={suggestedWidgets as SuggestedWidget[]}
|
||||
/>
|
||||
{!isEnabledForQueryBinding &&
|
||||
canEditPage &&
|
||||
hasResponse &&
|
||||
Object.keys(widgets).length > 1 && (
|
||||
<Collapsible label="Connect widget">
|
||||
<SnipingWrapper>
|
||||
<Button
|
||||
className={"t--select-in-canvas"}
|
||||
kind="secondary"
|
||||
onClick={handleBindData}
|
||||
size="md"
|
||||
>
|
||||
Select widget
|
||||
</Button>
|
||||
</SnipingWrapper>
|
||||
</Collapsible>
|
||||
)}
|
||||
{showSuggestedWidgets ? (
|
||||
<SchemaSideBarSection height={40} marginTop={12}>
|
||||
<SuggestedWidgets
|
||||
actionName={actionName}
|
||||
hasWidgets={hasWidgets}
|
||||
suggestedWidgets={suggestedWidgets as SuggestedWidget[]}
|
||||
/>
|
||||
</SchemaSideBarSection>
|
||||
) : (
|
||||
isEnabledForQueryBinding && (
|
||||
<DisabledCollapsible
|
||||
label={createMessage(BINDING_SECTION_LABEL)}
|
||||
tooltipLabel={createMessage(BINDINGS_DISABLED_TOOLTIP)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</SideBar>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
82
app/client/src/components/featureWalkthrough/index.tsx
Normal file
82
app/client/src/components/featureWalkthrough/index.tsx
Normal file
|
|
@ -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<FeatureParams | null>();
|
||||
const [feature, setFeature] = useState<FeatureParams[]>([]);
|
||||
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 (
|
||||
<WalkthroughContext.Provider
|
||||
value={{
|
||||
pushFeature,
|
||||
popFeature,
|
||||
feature,
|
||||
isOpened: !!activeWalkthrough,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{activeWalkthrough &&
|
||||
createPortal(
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<WalkthroughRenderer {...activeWalkthrough} />
|
||||
</Suspense>,
|
||||
document.body,
|
||||
)}
|
||||
</WalkthroughContext.Provider>
|
||||
);
|
||||
}
|
||||
92
app/client/src/components/featureWalkthrough/utils.ts
Normal file
92
app/client/src/components/featureWalkthrough/utils.ts
Normal file
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string, any>;
|
||||
};
|
||||
|
||||
type WalkthroughContextType = {
|
||||
pushFeature: (feature: FeatureParams) => void;
|
||||
popFeature: () => void;
|
||||
feature: FeatureParams[];
|
||||
isOpened: boolean;
|
||||
};
|
||||
|
||||
const WalkthroughContext = React.createContext<
|
||||
WalkthroughContextType | undefined
|
||||
>(undefined);
|
||||
|
||||
export default WalkthroughContext;
|
||||
|
|
@ -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<RefRectParams | null>(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 (
|
||||
<WalkthroughWrapper className="t--walkthrough-overlay">
|
||||
<SvgWrapper
|
||||
height={boundingRect.bh}
|
||||
width={boundingRect.bw}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<clipPath id={CLIPID}>
|
||||
<polygon
|
||||
// See the comments above the component declaration to understand the below points assignment.
|
||||
points={`
|
||||
0 0,
|
||||
0 ${boundingRect.bh},
|
||||
${boundingRect.tx} ${boundingRect.bh},
|
||||
${boundingRect.tx} ${boundingRect.ty},
|
||||
${boundingRect.tx + boundingRect.tw} ${boundingRect.ty},
|
||||
${boundingRect.tx + boundingRect.tw} ${
|
||||
boundingRect.ty + boundingRect.th
|
||||
},
|
||||
${boundingRect.tx} ${boundingRect.ty + boundingRect.th},
|
||||
${boundingRect.tx} ${boundingRect.bh},
|
||||
${boundingRect.bw} ${boundingRect.bh},
|
||||
${boundingRect.bw} 0
|
||||
`}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect
|
||||
style={{
|
||||
clipPath: 'url("#' + CLIPID + '")',
|
||||
fill: "currentcolor",
|
||||
height: boundingRect.bh,
|
||||
pointerEvents: "auto",
|
||||
width: boundingRect.bw,
|
||||
}}
|
||||
/>
|
||||
</SvgWrapper>
|
||||
<InstructionsComponent
|
||||
details={details}
|
||||
offset={offset}
|
||||
onClose={onDismissWalkthrough}
|
||||
targetId={targetId}
|
||||
/>
|
||||
</WalkthroughWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const InstructionsComponent = ({
|
||||
details,
|
||||
offset,
|
||||
onClose,
|
||||
targetId,
|
||||
}: {
|
||||
details?: FeatureDetails;
|
||||
offset?: OffsetType;
|
||||
targetId: string;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
if (!details) return null;
|
||||
|
||||
const positionAttr = getPosition({
|
||||
targetId,
|
||||
offset,
|
||||
});
|
||||
|
||||
return (
|
||||
<InstructionsWrapper style={{ ...positionAttr }}>
|
||||
<InstructionsHeaderWrapper>
|
||||
<Text kind="heading-s" renderAs="p">
|
||||
{details.title}
|
||||
</Text>
|
||||
<Icon name="close" onClick={onClose} size="md" />
|
||||
</InstructionsHeaderWrapper>
|
||||
<Text>{details.description}</Text>
|
||||
{details.imageURL && (
|
||||
<ImageWrapper>
|
||||
<img src={details.imageURL} />
|
||||
</ImageWrapper>
|
||||
)}
|
||||
</InstructionsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalkthroughRenderer;
|
||||
|
|
@ -282,7 +282,7 @@ function ConditionComponent(props: any, index: number) {
|
|||
props.onDeletePressed(index);
|
||||
}}
|
||||
size="md"
|
||||
startIcon="cross-line"
|
||||
startIcon="close"
|
||||
/>
|
||||
</ConditionBox>
|
||||
);
|
||||
|
|
@ -397,7 +397,7 @@ function ConditionBlock(props: any) {
|
|||
onDeletePressed(index);
|
||||
}}
|
||||
size="md"
|
||||
startIcon="cross-line"
|
||||
startIcon="close"
|
||||
top={"24px"}
|
||||
/>
|
||||
</GroupConditionBox>
|
||||
|
|
|
|||
|
|
@ -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 = "<<your_table_name>>";
|
||||
|
|
|
|||
|
|
@ -3015,7 +3015,7 @@ export const theme: Theme = {
|
|||
},
|
||||
},
|
||||
actionSidePane: {
|
||||
width: 265,
|
||||
width: 280,
|
||||
},
|
||||
onboarding: {
|
||||
statusBarHeight: 92,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export enum PluginName {
|
|||
SNOWFLAKE = "Snowflake",
|
||||
ARANGODB = "ArangoDB",
|
||||
REDSHIFT = "Redshift",
|
||||
SMTP = "SMTP",
|
||||
}
|
||||
|
||||
export enum PaginationType {
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<SomeWrapper>
|
||||
<ActionRightPane
|
||||
actionName={props.actionName}
|
||||
context={DatasourceStructureContext.API_EDITOR}
|
||||
datasourceId={props.datasourceId}
|
||||
entityDependencies={entityDependencies}
|
||||
hasConnections={hasDependencies}
|
||||
hasResponse={props.hasResponse}
|
||||
pluginId={props.pluginId}
|
||||
suggestedWidgets={props.suggestedWidgets}
|
||||
/>
|
||||
</SomeWrapper>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Props> {
|
|||
e.preventDefault();
|
||||
}}
|
||||
showFilterComponent={showFilterComponent}
|
||||
viewMode={viewMode}
|
||||
>
|
||||
{messages &&
|
||||
messages.map((msg, i) => {
|
||||
|
|
|
|||
|
|
@ -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<Props> {
|
|||
e.preventDefault();
|
||||
}}
|
||||
showFilterComponent={this.props.showFilterComponent}
|
||||
viewMode={this.props.viewMode}
|
||||
>
|
||||
{this.renderEditor()}
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -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<Props, State> {
|
|||
} = this.props;
|
||||
|
||||
const shouldViewMode = viewMode && !isInsideReconnectModal;
|
||||
// Check for specific form types first
|
||||
if (
|
||||
pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm &&
|
||||
!shouldViewMode
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<RestAPIDatasourceForm
|
||||
applicationId={this.props.applicationId}
|
||||
datasource={datasource}
|
||||
datasourceId={datasourceId}
|
||||
formData={formData}
|
||||
formName={formName}
|
||||
hiddenHeader={isInsideReconnectModal}
|
||||
isFormDirty={isFormDirty}
|
||||
isSaving={isSaving}
|
||||
location={location}
|
||||
pageId={pageId}
|
||||
pluginName={pluginName}
|
||||
pluginPackageName={pluginPackageName}
|
||||
showFilterComponent={this.state.filterParams.showFilterPane}
|
||||
/>
|
||||
{this.renderSaveDisacardModal()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Default to DB Editor Form
|
||||
return (
|
||||
<>
|
||||
<DataSourceEditorForm
|
||||
applicationId={this.props.applicationId}
|
||||
currentEnvironment={this.getEnvironmentId()}
|
||||
datasourceId={datasourceId}
|
||||
formConfig={formConfig}
|
||||
formData={formData}
|
||||
formName={DATASOURCE_DB_FORM}
|
||||
hiddenHeader={isInsideReconnectModal}
|
||||
isSaving={isSaving}
|
||||
pageId={pageId}
|
||||
pluginType={pluginType}
|
||||
setupConfig={this.setupConfig}
|
||||
showFilterComponent={this.state.filterParams.showFilterPane}
|
||||
viewMode={viewMode && !isInsideReconnectModal}
|
||||
/>
|
||||
{
|
||||
// Check for specific form types first
|
||||
pluginDatasourceForm ===
|
||||
DatasourceComponentTypes.RestAPIDatasourceForm &&
|
||||
!shouldViewMode ? (
|
||||
<RestAPIDatasourceForm
|
||||
applicationId={this.props.applicationId}
|
||||
datasource={datasource}
|
||||
datasourceId={datasourceId}
|
||||
formData={formData}
|
||||
formName={formName}
|
||||
hiddenHeader={isInsideReconnectModal}
|
||||
isFormDirty={isFormDirty}
|
||||
isSaving={isSaving}
|
||||
location={location}
|
||||
pageId={pageId}
|
||||
pluginName={pluginName}
|
||||
pluginPackageName={pluginPackageName}
|
||||
showFilterComponent={this.state.filterParams.showFilterPane}
|
||||
viewMode={shouldViewMode}
|
||||
/>
|
||||
) : (
|
||||
// Default to DB Editor Form
|
||||
<DataSourceEditorForm
|
||||
applicationId={this.props.applicationId}
|
||||
currentEnvironment={this.getEnvironmentId()}
|
||||
datasourceId={datasourceId}
|
||||
formConfig={formConfig}
|
||||
formData={formData}
|
||||
formName={DATASOURCE_DB_FORM}
|
||||
hiddenHeader={isInsideReconnectModal}
|
||||
isSaving={isSaving}
|
||||
pageId={pageId}
|
||||
pluginType={pluginType}
|
||||
setupConfig={this.setupConfig}
|
||||
showFilterComponent={this.state.filterParams.showFilterPane}
|
||||
viewMode={viewMode && !isInsideReconnectModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{this.renderSaveDisacardModal()}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -123,11 +123,11 @@ const Datasources = React.memo(() => {
|
|||
<Entity
|
||||
addButtonHelptext={createMessage(CREATE_DATASOURCE_TOOLTIP)}
|
||||
className={"group datasources"}
|
||||
entityId="datasources_section"
|
||||
entityId={pageId + "_datasources"}
|
||||
icon={null}
|
||||
isDefaultExpanded={
|
||||
isDatasourcesOpen === null || isDatasourcesOpen === undefined
|
||||
? false
|
||||
? true
|
||||
: isDatasourcesOpen
|
||||
}
|
||||
isSticky
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { getDatasource } from "selectors/entitiesSelector";
|
||||
import type { TreeDropdownOption } from "pages/Editor/Explorer/ContextMenu";
|
||||
import ContextMenu from "pages/Editor/Explorer/ContextMenu";
|
||||
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
|
||||
|
||||
export function DataSourceContextMenu(props: {
|
||||
datasourceId: string;
|
||||
|
|
@ -36,7 +37,12 @@ export function DataSourceContextMenu(props: {
|
|||
[dispatch, props.entityId],
|
||||
);
|
||||
const dispatchRefresh = useCallback(() => {
|
||||
dispatch(refreshDatasourceStructure(props.datasourceId));
|
||||
dispatch(
|
||||
refreshDatasourceStructure(
|
||||
props.datasourceId,
|
||||
DatasourceStructureContext.EXPLORER,
|
||||
),
|
||||
);
|
||||
}, [dispatch, props.datasourceId]);
|
||||
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
<DatasourceStructureContainer
|
||||
context={DatasourceStructureContext.EXPLORER}
|
||||
datasourceId={props.datasource.id}
|
||||
datasourceStructure={datasourceStructure}
|
||||
step={props.step}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useContext } from "react";
|
||||
import Entity, { EntityClassNames } from "../Entity";
|
||||
import { datasourceTableIcon } from "../ExplorerIcons";
|
||||
import QueryTemplates from "./QueryTemplates";
|
||||
|
|
@ -9,16 +9,26 @@ import { SIDEBAR_ID } from "constants/Explorer";
|
|||
import { hasCreateDatasourceActionPermission } from "@appsmith/utils/permissionHelpers";
|
||||
import { useSelector } from "react-redux";
|
||||
import type { AppState } from "@appsmith/reducers";
|
||||
import { getDatasource } from "selectors/entitiesSelector";
|
||||
import { getDatasource, getPlugin } from "selectors/entitiesSelector";
|
||||
import { getPagePermissions } from "selectors/editorSelectors";
|
||||
import { Menu, MenuTrigger, Button, Tooltip, MenuContent } from "design-system";
|
||||
import { SHOW_TEMPLATES, createMessage } from "@appsmith/constants/messages";
|
||||
import styled from "styled-components";
|
||||
import { DatasourceStructureContext } from "./DatasourceStructureContainer";
|
||||
import AnalyticsUtil from "utils/AnalyticsUtil";
|
||||
import type { Plugin } from "api/PluginApi";
|
||||
import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
|
||||
import { setFeatureFlagShownStatus } from "utils/storage";
|
||||
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
|
||||
|
||||
type DatasourceStructureProps = {
|
||||
dbStructure: DatasourceTable;
|
||||
step: number;
|
||||
datasourceId: string;
|
||||
context: DatasourceStructureContext;
|
||||
isDefaultOpen?: boolean;
|
||||
forceExpand?: boolean;
|
||||
currentActionId: string;
|
||||
};
|
||||
|
||||
const StyledMenuContent = styled(MenuContent)`
|
||||
|
|
@ -32,10 +42,17 @@ export function DatasourceStructure(props: DatasourceStructureProps) {
|
|||
const [active, setActive] = useState(false);
|
||||
useCloseMenuOnScroll(SIDEBAR_ID, active, () => setActive(false));
|
||||
|
||||
const { isOpened: isWalkthroughOpened, popFeature } =
|
||||
useContext(WalkthroughContext) || {};
|
||||
|
||||
const datasource = useSelector((state: AppState) =>
|
||||
getDatasource(state, props.datasourceId),
|
||||
);
|
||||
|
||||
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 ? (
|
||||
<Menu open={active}>
|
||||
<Tooltip
|
||||
content={createMessage(SHOW_TEMPLATES)}
|
||||
isDisabled={active}
|
||||
mouseLeaveDelay={0}
|
||||
placement="right"
|
||||
>
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
className={`button-icon t--template-menu-trigger ${EntityClassNames.CONTEXT_MENU}`}
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={() => setActive(!active)}
|
||||
startIcon="increase-control-v2"
|
||||
const onSelect = () => {
|
||||
setActive(false);
|
||||
};
|
||||
|
||||
const onEntityClick = () => {
|
||||
AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_TABLE_SELECT", {
|
||||
datasourceId: props.datasourceId,
|
||||
pluginName: plugin?.name,
|
||||
});
|
||||
|
||||
canCreateDatasourceActions && setActive(!active);
|
||||
|
||||
dbStructure.templates.length === 0 &&
|
||||
isWalkthroughOpened &&
|
||||
closeWalkthrough();
|
||||
};
|
||||
|
||||
const closeWalkthrough = () => {
|
||||
popFeature && popFeature();
|
||||
setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
|
||||
};
|
||||
|
||||
const lightningMenu =
|
||||
canCreateDatasourceActions && dbStructure.templates.length > 0 ? (
|
||||
<Menu open={active}>
|
||||
<Tooltip
|
||||
content={createMessage(SHOW_TEMPLATES)}
|
||||
isDisabled={active}
|
||||
mouseLeaveDelay={0}
|
||||
placement="right"
|
||||
>
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
className={`button-icon t--template-menu-trigger ${EntityClassNames.CONTEXT_MENU}`}
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
onClick={() => setActive(!active)}
|
||||
startIcon={
|
||||
props.context !== DatasourceStructureContext.EXPLORER
|
||||
? "add-line"
|
||||
: "increase-control-v2"
|
||||
}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
</Tooltip>
|
||||
<StyledMenuContent
|
||||
align="start"
|
||||
className="t--structure-template-menu-popover"
|
||||
onInteractOutside={() => setActive(false)}
|
||||
side="right"
|
||||
>
|
||||
<QueryTemplates
|
||||
context={props.context}
|
||||
currentActionId={props.currentActionId}
|
||||
datasourceId={props.datasourceId}
|
||||
onSelect={onSelect}
|
||||
templates={dbStructure.templates}
|
||||
/>
|
||||
</MenuTrigger>
|
||||
</Tooltip>
|
||||
<StyledMenuContent
|
||||
align="start"
|
||||
className="t--structure-template-menu-popover"
|
||||
onInteractOutside={() => setActive(false)}
|
||||
side="right"
|
||||
>
|
||||
<QueryTemplates
|
||||
datasourceId={props.datasourceId}
|
||||
onSelect={() => setActive(false)}
|
||||
templates={dbStructure.templates}
|
||||
/>
|
||||
</StyledMenuContent>
|
||||
</Menu>
|
||||
) : null;
|
||||
</StyledMenuContent>
|
||||
</Menu>
|
||||
) : null;
|
||||
|
||||
if (dbStructure.templates) templateMenu = lightningMenu;
|
||||
const columnsAndKeys = dbStructure.columns.concat(dbStructure.keys);
|
||||
|
||||
return (
|
||||
<Entity
|
||||
action={() => 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 (
|
||||
<DatasourceField
|
||||
field={field}
|
||||
key={`${field.name}${index}`}
|
||||
step={props.step + 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<>
|
||||
{columnsAndKeys.map((field, index) => {
|
||||
return (
|
||||
<DatasourceField
|
||||
field={field}
|
||||
key={`${field.name}${index}`}
|
||||
step={props.step + 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</Entity>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Props> = <div />;
|
||||
const isLoading = useSelector((state: AppState) =>
|
||||
getIsFetchingDatasourceStructure(state, props.datasourceId),
|
||||
);
|
||||
let view: ReactElement<Props> | JSX.Element = <div />;
|
||||
|
||||
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 && (
|
||||
<DatasourceStructureSearchContainer>
|
||||
<Input
|
||||
className="datasourceStructure-search"
|
||||
onChange={(value) => handleOnChange(value)}
|
||||
placeholder={createMessage(
|
||||
DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT,
|
||||
)}
|
||||
size={"md"}
|
||||
startIcon="search"
|
||||
type="text"
|
||||
/>
|
||||
</DatasourceStructureSearchContainer>
|
||||
)}
|
||||
{!!datasourceStructure?.tables?.length &&
|
||||
datasourceStructure.tables.map((structure: DatasourceTable) => {
|
||||
return (
|
||||
<DatasourceStructure
|
||||
context={props.context}
|
||||
currentActionId={props.currentActionId || ""}
|
||||
datasourceId={props.datasourceId}
|
||||
dbStructure={structure}
|
||||
key={`${props.datasourceId}${structure.name}`}
|
||||
forceExpand={hasSearchedOccured}
|
||||
key={`${props.datasourceId}${structure.name}-${props.context}`}
|
||||
step={props.step + 1}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})}
|
||||
|
||||
{!datasourceStructure?.tables?.length && (
|
||||
<Text kind="body-s" renderAs="p">
|
||||
{createMessage(TABLE_OR_COLUMN_NOT_FOUND)}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
view = (
|
||||
<EntityPlaceholder step={props.step + 1}>
|
||||
{props.datasourceStructure &&
|
||||
props.datasourceStructure.error &&
|
||||
props.datasourceStructure.error.message &&
|
||||
props.datasourceStructure.error.message !== "null"
|
||||
? props.datasourceStructure.error.message
|
||||
: createMessage(SCHEMA_NOT_AVAILABLE)}
|
||||
</EntityPlaceholder>
|
||||
);
|
||||
if (props.context !== DatasourceStructureContext.EXPLORER) {
|
||||
view = (
|
||||
<DatasourceStructureNotFound
|
||||
datasourceId={props.datasourceId}
|
||||
error={
|
||||
!!props.datasourceStructure &&
|
||||
"error" in props.datasourceStructure
|
||||
? props.datasourceStructure.error
|
||||
: { message: createMessage(SCHEMA_NOT_AVAILABLE) }
|
||||
}
|
||||
pluginName={props?.pluginName || ""}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
view = (
|
||||
<EntityPlaceholder step={props.step + 1}>
|
||||
{props.datasourceStructure &&
|
||||
props.datasourceStructure.error &&
|
||||
props.datasourceStructure.error.message &&
|
||||
props.datasourceStructure.error.message !== "null"
|
||||
? props.datasourceStructure.error.message
|
||||
: createMessage(SCHEMA_NOT_AVAILABLE)}
|
||||
</EntityPlaceholder>
|
||||
);
|
||||
}
|
||||
}
|
||||
} 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 = <DatasourceStructureLoadingContainer />;
|
||||
}
|
||||
|
||||
return view;
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement, MouseEvent>) => {
|
||||
event.stopPropagation();
|
||||
dispatch(
|
||||
refreshDatasourceStructure(
|
||||
props.datasourceId,
|
||||
DatasourceStructureContext.QUERY_EDITOR,
|
||||
),
|
||||
);
|
||||
},
|
||||
[dispatch, props.datasourceId],
|
||||
);
|
||||
|
||||
return (
|
||||
<HeaderWrapper>
|
||||
<Text kind="heading-xs" renderAs="h3">
|
||||
{createMessage(SCHEMA_LABEL)}
|
||||
</Text>
|
||||
<div onClick={(event) => dispatchRefresh(event)}>
|
||||
<Icon name="refresh" size={"md"} />
|
||||
</div>
|
||||
</HeaderWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<LoadingContainer>
|
||||
<SpinnerWrapper>
|
||||
<Spinner size={"sm"} />
|
||||
</SpinnerWrapper>
|
||||
<Text kind="body-m" renderAs="p">
|
||||
{createMessage(LOADING_SCHEMA)}
|
||||
</Text>
|
||||
</LoadingContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasourceStructureLoadingContainer;
|
||||
|
|
@ -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 (
|
||||
<NotFoundContainer>
|
||||
{error?.message && (
|
||||
<NotFoundText kind="body-s" renderAs="p">
|
||||
{error.message}
|
||||
</NotFoundText>
|
||||
)}
|
||||
<ButtonWrapper>
|
||||
<Button kind="secondary" onClick={editDatasource} size={"md"}>
|
||||
{createMessage(EDIT_DATASOURCE)}
|
||||
</Button>
|
||||
</ButtonWrapper>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatasourceStructureNotFound;
|
||||
|
|
@ -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<QueryAction> = {
|
||||
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 (
|
||||
<MenuItem
|
||||
<TemplateMenuItem
|
||||
key={template.title}
|
||||
onSelect={() => {
|
||||
createQueryAction(template);
|
||||
if (props.currentActionId) {
|
||||
updateQueryAction(template);
|
||||
} else {
|
||||
createQueryAction(template);
|
||||
}
|
||||
props.onSelect();
|
||||
}}
|
||||
>
|
||||
{template.title}
|
||||
</MenuItem>
|
||||
</TemplateMenuItem>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ export type EntityProps = {
|
|||
export const Entity = forwardRef(
|
||||
(props: EntityProps, ref: React.Ref<HTMLDivElement>) => {
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</AddButtonWrapper>
|
||||
</Tooltip>
|
||||
}
|
||||
entityId="library_section"
|
||||
entityId={pageId + "_library_section"}
|
||||
icon={null}
|
||||
isDefaultExpanded={isOpen}
|
||||
isSticky
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -231,10 +231,6 @@ function GeneratePageForm() {
|
|||
useState<string>("");
|
||||
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<boolean>(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
)}
|
||||
</SecondaryWrapper>
|
||||
</div>
|
||||
<SidebarWrapper
|
||||
show={(hasDependencies || !!output) && !guidedTourEnabled}
|
||||
>
|
||||
<SidebarWrapper show={shouldOpenActionPaneByDefault}>
|
||||
<ActionRightPane
|
||||
actionName={actionName}
|
||||
context={DatasourceStructureContext.QUERY_EDITOR}
|
||||
datasourceId={props.datasourceId}
|
||||
entityDependencies={entityDependencies}
|
||||
hasConnections={hasDependencies}
|
||||
hasResponse={!!output}
|
||||
pluginId={props.pluginId}
|
||||
suggestedWidgets={executedQueryData?.suggestedWidgets}
|
||||
/>
|
||||
</SidebarWrapper>
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ class QueryEditor extends React.Component<Props> {
|
|||
return (
|
||||
<QueryEditorForm
|
||||
dataSources={dataSources}
|
||||
datasourceId={this.props.datasourceId}
|
||||
editorConfig={editorConfig}
|
||||
executedQueryData={responses[actionId]}
|
||||
formData={this.props.formData}
|
||||
|
|
@ -261,6 +262,7 @@ class QueryEditor extends React.Component<Props> {
|
|||
onCreateDatasourceClick={this.onCreateDatasourceClick}
|
||||
onDeleteClick={this.handleDeleteClick}
|
||||
onRunClick={this.handleRunClick}
|
||||
pluginId={this.props.pluginId}
|
||||
runErrorMessage={runErrorMessage[actionId]}
|
||||
settingConfig={settingConfig}
|
||||
uiComponent={uiComponent}
|
||||
|
|
|
|||
|
|
@ -437,6 +437,7 @@ class DatasourceSaaSEditor extends JSONtoForm<Props, State> {
|
|||
e.preventDefault();
|
||||
}}
|
||||
showFilterComponent={false}
|
||||
viewMode={viewMode}
|
||||
>
|
||||
{(!viewMode || createFlow || isInsideReconnectModal) && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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<T extends URLBuilderParams>(
|
|||
|
||||
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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ export interface DatasourceDataState {
|
|||
loading: boolean;
|
||||
isTesting: boolean;
|
||||
isListing: boolean; // fetching unconfigured datasource list
|
||||
fetchingDatasourceStructure: boolean;
|
||||
isRefreshingStructure: boolean;
|
||||
fetchingDatasourceStructure: Record<string, boolean>;
|
||||
structure: Record<string, DatasourceStructure>;
|
||||
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]: (
|
||||
|
|
|
|||
|
|
@ -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<Datasource>(
|
||||
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<Datasource>(
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, any> | 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<string, any> | 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<string, any> | 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<string, any> | 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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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<WidgetSuggestionDTO> getWidgetsForTypeArray(List<String> fields, List<String> numericFields) {
|
||||
List<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> getWidgetsForTypeNumber() {
|
||||
List<WidgetSuggestionDTO> 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <T extends BaseDomain> Mono<T> sendCreateEvent(T object, Map<String, Object> extraProperties) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -369,7 +369,6 @@ public class ActionExecutionSolutionCETest {
|
|||
mockResult.setHeaders(objectMapper.valueToTree(Map.of("response-header-key", "response-header-value")));
|
||||
|
||||
List<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> 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<WidgetSuggestionDTO> widgetTypeList = new ArrayList<>();
|
||||
widgetTypeList.add(WidgetSuggestionHelper.getWidget(WidgetType.TEXT_WIDGET));
|
||||
mockResult.setSuggestedWidgets(widgetTypeList);
|
||||
|
||||
ActionDTO action = new ActionDTO();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user