addWidget(suggestedWidget, widgetInfo)}
- >
-
-
- {widgetInfo.image && (
-
})
- )}
+ if (!widgetInfo || widget?.type !== "TABLE_WIDGET_V2")
+ return null;
+
+ return (
+
handleBindData(widgetKey)}
+ >
+
+
+ {renderWidgetImage(widgetInfo.existingImage)}
+ {renderWidgetItem(
+ widgetInfo.icon,
+ widget.widgetName,
+ "body-s",
+ )}
+
+
+
+ );
+ })}
+
+ }
+
+ )}
+
+ {renderHeading(addNewWidgetLabel, addNewWidgetSubLabel)}
+
+ {props.suggestedWidgets.map((suggestedWidget) => {
+ const widgetInfo: WidgetBindingInfo | undefined =
+ WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
+
+ if (!widgetInfo) return null;
+
+ return (
+ addWidget(suggestedWidget, widgetInfo)}
+ >
+
+ {renderWidgetItem(
+ widgetInfo.icon,
+ widgetInfo.widgetName,
+ "body-m",
+ )}
+
+
+ );
+ })}
+
+
+
+ ) : (
+
+
+ {props.suggestedWidgets.map((suggestedWidget) => {
+ const widgetInfo: WidgetBindingInfo | undefined =
+ WIDGET_DATA_FIELD_MAP[suggestedWidget.type];
+
+ if (!widgetInfo) return null;
+
+ return (
+ addWidget(suggestedWidget, widgetInfo)}
+ >
+
+
+ {renderWidgetImage(widgetInfo.image)}
+
+
-
-
- );
- })}
-
-
+ );
+ })}
+
+
+ )}
+
);
}
diff --git a/app/client/src/components/editorComponents/ActionRightPane/index.tsx b/app/client/src/components/editorComponents/ActionRightPane/index.tsx
index 619f98415b..561befe214 100644
--- a/app/client/src/components/editorComponents/ActionRightPane/index.tsx
+++ b/app/client/src/components/editorComponents/ActionRightPane/index.tsx
@@ -1,8 +1,8 @@
-import React, { useMemo } from "react";
+import React, { useContext, useMemo } from "react";
import styled from "styled-components";
import { Collapse, Classes as BPClasses } from "@blueprintjs/core";
import { Classes, getTypographyByKey } from "design-system-old";
-import { Button, Icon, Link } from "design-system";
+import { Button, Divider, Icon, Link, Text } from "design-system";
import { useState } from "react";
import Connections from "./Connections";
import SuggestedWidgets from "./SuggestedWidgets";
@@ -17,8 +17,12 @@ import type { AppState } from "@appsmith/reducers";
import { getDependenciesFromInverseDependencies } from "../Debugger/helpers";
import {
BACK_TO_CANVAS,
+ BINDINGS_DISABLED_TOOLTIP,
+ BINDING_SECTION_LABEL,
createMessage,
NO_CONNECTIONS,
+ SCHEMA_WALKTHROUGH_DESC,
+ SCHEMA_WALKTHROUGH_TITLE,
} from "@appsmith/constants/messages";
import type {
SuggestedWidget,
@@ -31,16 +35,39 @@ import {
} from "selectors/editorSelectors";
import { builderURL } from "RouteBuilder";
import { hasManagePagePermission } from "@appsmith/utils/permissionHelpers";
+import DatasourceStructureHeader from "pages/Editor/Explorer/Datasources/DatasourceStructureHeader";
+import { DatasourceStructureContainer as DataStructureList } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer";
+import { DatasourceStructureContext } from "pages/Editor/Explorer/Datasources/DatasourceStructureContainer";
+import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
+import {
+ AB_TESTING_EVENT_KEYS,
+ FEATURE_FLAG,
+} from "@appsmith/entities/FeatureFlag";
+import {
+ getDatasourceStructureById,
+ getPluginDatasourceComponentFromId,
+ getPluginNameFromId,
+} from "selectors/entitiesSelector";
+import { DatasourceComponentTypes } from "api/PluginApi";
+import { fetchDatasourceStructure } from "actions/datasourceActions";
+import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
+import {
+ getFeatureFlagShownStatus,
+ isUserSignedUpFlagSet,
+ setFeatureFlagShownStatus,
+} from "utils/storage";
+import { PluginName } from "entities/Action";
+import { getCurrentUser } from "selectors/usersSelectors";
+import { Tooltip } from "design-system";
+import { ASSETS_CDN_URL } from "constants/ThirdPartyConstants";
+
+const SCHEMA_GUIDE_GIF = `${ASSETS_CDN_URL}/schema.gif`;
+
+const SCHEMA_SECTION_ID = "t--api-right-pane-schema";
const SideBar = styled.div`
height: 100%;
width: 100%;
- -webkit-animation: slide-left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
- animation: slide-left 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
-
- & > div {
- margin-top: ${(props) => props.theme.spaces[11]}px;
- }
& > a {
margin-top: 0;
@@ -90,15 +117,30 @@ const SideBar = styled.div`
const BackToCanvasLink = styled(Link)`
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
margin-top: ${(props) => props.theme.spaces[11]}px;
+ margin-bottom: ${(props) => props.theme.spaces[11]}px;
`;
const Label = styled.span`
cursor: pointer;
`;
-const CollapsibleWrapper = styled.div<{ isOpen: boolean }>`
+const CollapsibleWrapper = styled.div<{
+ isOpen: boolean;
+ isDisabled?: boolean;
+}>`
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ ${(props) => !!props.isDisabled && `opacity: 0.6`};
+
+ &&&&&& .${BPClasses.COLLAPSE} {
+ flex-grow: 1;
+ overflow-y: auto !important;
+ }
+
.${BPClasses.COLLAPSE_BODY} {
padding-top: ${(props) => props.theme.spaces[3]}px;
+ height: 100%;
}
& > .icon-text:first-child {
@@ -142,14 +184,36 @@ const Placeholder = styled.div`
text-align: center;
`;
+const DataStructureListWrapper = styled.div`
+ overflow-y: scroll;
+ height: 100%;
+`;
+
+const SchemaSideBarSection = styled.div<{ height: number; marginTop?: number }>`
+ margin-top: ${(props) => props?.marginTop && `${props.marginTop}px`};
+ height: auto;
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+ ${(props) => props.height && `max-height: ${props.height}%;`}
+`;
+
type CollapsibleProps = {
expand?: boolean;
children: ReactNode;
label: string;
+ customLabelComponent?: JSX.Element;
+ isDisabled?: boolean;
+};
+
+type DisabledCollapsibleProps = {
+ label: string;
+ tooltipLabel?: string;
};
export function Collapsible({
children,
+ customLabelComponent,
expand = true,
label,
}: CollapsibleProps) {
@@ -162,8 +226,14 @@ export function Collapsible({
return (
{children}
@@ -172,6 +242,24 @@ export function Collapsible({
);
}
+export function DisabledCollapsible({
+ label,
+ tooltipLabel = "",
+}: DisabledCollapsibleProps) {
+ return (
+
+
+
+
+
+ );
+}
+
export function useEntityDependencies(actionName: string) {
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
const entityDependencies = useMemo(
@@ -194,9 +282,12 @@ export function useEntityDependencies(actionName: string) {
function ActionSidebar({
actionName,
+ context,
+ datasourceId,
entityDependencies,
hasConnections,
hasResponse,
+ pluginId,
suggestedWidgets,
}: {
actionName: string;
@@ -207,11 +298,16 @@ function ActionSidebar({
directDependencies: string[];
inverseDependencies: string[];
} | null;
+ datasourceId: string;
+ pluginId: string;
+ context: DatasourceStructureContext;
}) {
const dispatch = useDispatch();
const widgets = useSelector(getWidgets);
const applicationId = useSelector(getCurrentApplicationId);
const pageId = useSelector(getCurrentPageId);
+ const user = useSelector(getCurrentUser);
+ const { pushFeature } = useContext(WalkthroughContext) || {};
const params = useParams<{
pageId: string;
apiId?: string;
@@ -232,6 +328,100 @@ function ActionSidebar({
);
};
+ const pluginName = useSelector((state) =>
+ getPluginNameFromId(state, pluginId || ""),
+ );
+
+ const pluginDatasourceForm = useSelector((state) =>
+ getPluginDatasourceComponentFromId(state, pluginId || ""),
+ );
+
+ // A/B feature flag for datasource structure.
+ const isEnabledForDSSchema = useSelector((state) =>
+ selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_schema_enabled),
+ );
+
+ // A/B feature flag for query binding.
+ const isEnabledForQueryBinding = useSelector((state) =>
+ selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled),
+ );
+
+ const datasourceStructure = useSelector((state) =>
+ getDatasourceStructureById(state, datasourceId),
+ );
+
+ useEffect(() => {
+ if (
+ datasourceId &&
+ datasourceStructure === undefined &&
+ pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm
+ ) {
+ dispatch(
+ fetchDatasourceStructure(
+ datasourceId,
+ true,
+ DatasourceStructureContext.QUERY_EDITOR,
+ ),
+ );
+ }
+ }, []);
+
+ const checkAndShowWalkthrough = async () => {
+ const isFeatureWalkthroughShown = await getFeatureFlagShownStatus(
+ FEATURE_FLAG.ab_ds_schema_enabled,
+ );
+
+ const isNewUser = user && (await isUserSignedUpFlagSet(user.email));
+ // Adding walkthrough tutorial
+ isNewUser &&
+ !isFeatureWalkthroughShown &&
+ pushFeature &&
+ pushFeature({
+ targetId: SCHEMA_SECTION_ID,
+ onDismiss: async () => {
+ AnalyticsUtil.logEvent("WALKTHROUGH_DISMISSED", {
+ [AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
+ FEATURE_FLAG.ab_ds_schema_enabled,
+ [AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema,
+ });
+ await setFeatureFlagShownStatus(
+ FEATURE_FLAG.ab_ds_schema_enabled,
+ true,
+ );
+ },
+ details: {
+ title: createMessage(SCHEMA_WALKTHROUGH_TITLE),
+ description: createMessage(SCHEMA_WALKTHROUGH_DESC),
+ imageURL: SCHEMA_GUIDE_GIF,
+ },
+ offset: {
+ position: "left",
+ left: -40,
+ highlightPad: 5,
+ indicatorLeft: -3,
+ style: {
+ transform: "none",
+ },
+ },
+ eventParams: {
+ [AB_TESTING_EVENT_KEYS.abTestingFlagLabel]:
+ FEATURE_FLAG.ab_ds_schema_enabled,
+ [AB_TESTING_EVENT_KEYS.abTestingFlagValue]: isEnabledForDSSchema,
+ },
+ });
+ };
+
+ const showSchema =
+ isEnabledForDSSchema &&
+ pluginDatasourceForm !== DatasourceComponentTypes.RestAPIDatasourceForm &&
+ pluginName !== PluginName.SMTP;
+
+ useEffect(() => {
+ if (showSchema) {
+ checkAndShowWalkthrough();
+ }
+ }, [showSchema]);
+
const hasWidgets = Object.keys(widgets).length > 1;
const pagePermissions = useSelector(getPagePermissions);
@@ -242,7 +432,13 @@ function ActionSidebar({
canEditPage && hasResponse && suggestedWidgets && !!suggestedWidgets.length;
const showSnipingMode = hasResponse && hasWidgets;
- if (!hasConnections && !showSuggestedWidgets && !showSnipingMode) {
+ if (
+ !hasConnections &&
+ !showSuggestedWidgets &&
+ !showSnipingMode &&
+ // putting this here to make the placeholder only appear for rest APIs.
+ pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm
+ ) {
return {createMessage(NO_CONNECTIONS)};
}
@@ -257,33 +453,69 @@ function ActionSidebar({
{createMessage(BACK_TO_CANVAS)}
- {hasConnections && (
+ {showSchema && (
+
+
+ }
+ expand={!showSuggestedWidgets}
+ label="Schema"
+ >
+
+
+
+
+
+ )}
+
+ {showSchema && isEnabledForQueryBinding && }
+
+ {hasConnections && !isEnabledForQueryBinding && (
)}
- {canEditPage && hasResponse && Object.keys(widgets).length > 1 && (
-
- {/*Go to canvas and select widgets
*/}
-
-
-
-
- )}
- {showSuggestedWidgets && (
-
+ {!isEnabledForQueryBinding &&
+ canEditPage &&
+ hasResponse &&
+ Object.keys(widgets).length > 1 && (
+
+
+
+
+
+ )}
+ {showSuggestedWidgets ? (
+
+
+
+ ) : (
+ isEnabledForQueryBinding && (
+
+ )
)}
);
diff --git a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx
index 3b323493a4..258bd9cdde 100644
--- a/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx
+++ b/app/client/src/components/editorComponents/WidgetQueryGeneratorForm/CommonControls/TableOrSpreadsheetDropdown/useTableOrSpreadsheet.tsx
@@ -45,8 +45,8 @@ export function useTableOrSpreadsheet() {
const isFetchingSpreadsheets = useSelector(getIsFetchingGsheetSpreadsheets);
- const isFetchingDatasourceStructure = useSelector(
- getIsFetchingDatasourceStructure,
+ const isFetchingDatasourceStructure = useSelector((state: AppState) =>
+ getIsFetchingDatasourceStructure(state, config.datasource),
);
const selectedDatasourcePluginPackageName = useSelector((state: AppState) =>
diff --git a/app/client/src/components/featureWalkthrough/index.tsx b/app/client/src/components/featureWalkthrough/index.tsx
new file mode 100644
index 0000000000..eedbbd6195
--- /dev/null
+++ b/app/client/src/components/featureWalkthrough/index.tsx
@@ -0,0 +1,82 @@
+import React, { lazy, useEffect, useState, Suspense } from "react";
+import type { FeatureParams } from "./walkthroughContext";
+import WalkthroughContext from "./walkthroughContext";
+import { createPortal } from "react-dom";
+import { hideIndicator } from "pages/Editor/GuidedTour/utils";
+import { retryPromise } from "utils/AppsmithUtils";
+import { useLocation } from "react-router-dom";
+
+const WalkthroughRenderer = lazy(() => {
+ return retryPromise(
+ () =>
+ import(
+ /* webpackChunkName: "walkthrough-renderer" */ "./walkthroughRenderer"
+ ),
+ );
+});
+
+const LoadingFallback = () => null;
+
+export default function Walkthrough({ children }: any) {
+ const [activeWalkthrough, setActiveWalkthrough] =
+ useState();
+ const [feature, setFeature] = useState([]);
+ const location = useLocation();
+
+ const pushFeature = (value: FeatureParams) => {
+ const alreadyExists = feature.some((f) => f.targetId === value.targetId);
+ if (!alreadyExists) {
+ if (Array.isArray(value)) {
+ setFeature((e) => [...e, ...value]);
+ } else {
+ setFeature((e) => [...e, value]);
+ }
+ }
+ updateActiveWalkthrough();
+ };
+
+ const popFeature = () => {
+ hideIndicator();
+ setFeature((e) => {
+ e.shift();
+ return [...e];
+ });
+ };
+
+ const updateActiveWalkthrough = () => {
+ if (feature.length > 0) {
+ const highlightArea = document.querySelector(`#${feature[0].targetId}`);
+ if (highlightArea) {
+ setActiveWalkthrough(feature[0]);
+ } else {
+ setActiveWalkthrough(null);
+ }
+ } else {
+ setActiveWalkthrough(null);
+ }
+ };
+
+ useEffect(() => {
+ if (feature.length > -1) updateActiveWalkthrough();
+ }, [feature.length, location]);
+
+ return (
+
+ {children}
+ {activeWalkthrough &&
+ createPortal(
+ }>
+
+ ,
+ document.body,
+ )}
+
+ );
+}
diff --git a/app/client/src/components/featureWalkthrough/utils.ts b/app/client/src/components/featureWalkthrough/utils.ts
new file mode 100644
index 0000000000..9f737542ea
--- /dev/null
+++ b/app/client/src/components/featureWalkthrough/utils.ts
@@ -0,0 +1,92 @@
+import type { OffsetType, PositionType } from "./walkthroughContext";
+
+const DEFAULT_POSITION: PositionType = "top";
+export const PADDING_HIGHLIGHT = 10;
+
+type PositionCalculator = {
+ offset?: OffsetType;
+ targetId: string;
+};
+
+export function getPosition({ offset, targetId }: PositionCalculator) {
+ const target = document.querySelector(`#${targetId}`);
+ const bodyCoordinates = document.body.getBoundingClientRect();
+ if (!target) return null;
+ let coordinates;
+ if (target) {
+ coordinates = target.getBoundingClientRect();
+ }
+
+ if (!coordinates) return null;
+
+ const offsetValues = { top: offset?.top || 0, left: offset?.left || 0 };
+ const extraStyles = offset?.style || {};
+
+ /**
+ * . - - - - - - - - - - - - - - - - - .
+ * | Body |
+ * | |
+ * | . - - - - - - - - - - . |
+ * | | Offset | |
+ * | | . - - - - - - - . | |
+ * | | | / / / / / / / | | |
+ * | | | / / /Target/ /| | |
+ * | | | / / / / / / / | | |
+ * | | . - - - - - - - . | |
+ * | | | |
+ * | . _ _ _ _ _ _ _ _ _ _ . |
+ * | |
+ * . - - - - - - - - - - - - - - - - - .
+ */
+
+ switch (offset?.position || DEFAULT_POSITION) {
+ case "top":
+ return {
+ bottom:
+ bodyCoordinates.height -
+ coordinates.top -
+ offsetValues.top +
+ PADDING_HIGHLIGHT +
+ "px",
+ left: coordinates.left + offsetValues.left + PADDING_HIGHLIGHT + "px",
+ transform: "translateX(-50%)",
+ ...extraStyles,
+ };
+ case "bottom":
+ return {
+ top:
+ coordinates.height +
+ coordinates.top +
+ offsetValues.top +
+ PADDING_HIGHLIGHT +
+ "px",
+ left: coordinates.left + offsetValues.left - PADDING_HIGHLIGHT + "px",
+ transform: "translateX(-50%)",
+ ...extraStyles,
+ };
+ case "left":
+ return {
+ top: coordinates.top + offsetValues.top - PADDING_HIGHLIGHT + "px",
+ right:
+ bodyCoordinates.width -
+ coordinates.left -
+ offsetValues.left +
+ PADDING_HIGHLIGHT +
+ "px",
+ transform: "translateY(-50%)",
+ ...extraStyles,
+ };
+ case "right":
+ return {
+ top: coordinates.top + offsetValues.top - PADDING_HIGHLIGHT + "px",
+ left:
+ coordinates.left +
+ coordinates.width +
+ offsetValues.left +
+ PADDING_HIGHLIGHT +
+ "px",
+ transform: "translateY(-50%)",
+ ...extraStyles,
+ };
+ }
+}
diff --git a/app/client/src/components/featureWalkthrough/walkthroughContext.tsx b/app/client/src/components/featureWalkthrough/walkthroughContext.tsx
new file mode 100644
index 0000000000..9d31d38eef
--- /dev/null
+++ b/app/client/src/components/featureWalkthrough/walkthroughContext.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+
+export type PositionType = "top" | "bottom" | "left" | "right";
+
+export type OffsetType = {
+ // Position for the instructions and indicator
+ position?: PositionType;
+ // Adds an offset to top or bottom properties (of Instruction div) depending upon the position
+ top?: number;
+ // Adds an offset to left or right properties (of Instruction div) depending upon the position
+ left?: number;
+ // Style for the Instruction div overrides all other styles
+ style?: any;
+ // Indicator top and left offsets
+ indicatorTop?: number;
+ indicatorLeft?: number;
+ // container offset for highlight
+ highlightPad?: number;
+};
+
+export type FeatureDetails = {
+ // Title to show on the instruction screen
+ title: string;
+ // Description to show on the instruction screen
+ description: string;
+ // Gif or Image to give a walkthrough
+ imageURL?: string;
+};
+
+export type FeatureParams = {
+ // To execute a function on dismissing the tutorial walkthrough.
+ onDismiss?: () => void;
+ // Target Id without # to highlight the feature
+ targetId: string;
+ // Details for the instruction screen
+ details?: FeatureDetails;
+ // Offsets for the instruction screen and the indicator
+ offset?: OffsetType;
+ // Event params
+ eventParams?: Record;
+};
+
+type WalkthroughContextType = {
+ pushFeature: (feature: FeatureParams) => void;
+ popFeature: () => void;
+ feature: FeatureParams[];
+ isOpened: boolean;
+};
+
+const WalkthroughContext = React.createContext<
+ WalkthroughContextType | undefined
+>(undefined);
+
+export default WalkthroughContext;
diff --git a/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx b/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx
new file mode 100644
index 0000000000..97f52dfec2
--- /dev/null
+++ b/app/client/src/components/featureWalkthrough/walkthroughRenderer.tsx
@@ -0,0 +1,258 @@
+import { Icon, Text } from "design-system";
+import { showIndicator } from "pages/Editor/GuidedTour/utils";
+import React, { useContext, useEffect, useState } from "react";
+import styled from "styled-components";
+import { PADDING_HIGHLIGHT, getPosition } from "./utils";
+import type {
+ FeatureDetails,
+ FeatureParams,
+ OffsetType,
+} from "./walkthroughContext";
+import WalkthroughContext from "./walkthroughContext";
+import AnalyticsUtil from "utils/AnalyticsUtil";
+
+const CLIPID = "clip__feature";
+const Z_INDEX = 1000;
+
+const WalkthroughWrapper = styled.div`
+ left: 0px;
+ top: 0px;
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ color: rgb(0, 0, 0, 0.7);
+ z-index: ${Z_INDEX};
+ // This allows the user to click on the target element rather than the overlay div
+ pointer-events: none;
+`;
+
+const SvgWrapper = styled.svg`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+`;
+
+const InstructionsWrapper = styled.div`
+ padding: var(--ads-v2-spaces-4);
+ position: absolute;
+ background: white;
+ display: flex;
+ flex-direction: column;
+ width: 296px;
+ pointer-events: auto;
+ border-radius: var(--ads-radius-1);
+`;
+
+const ImageWrapper = styled.div`
+ border-radius: var(--ads-radius-1);
+ background: #f1f5f9;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 8px;
+ padding: var(--ads-v2-spaces-7);
+ img {
+ max-height: 220px;
+ }
+`;
+
+const InstructionsHeaderWrapper = styled.div`
+ display: flex;
+ p {
+ flex-grow: 1;
+ }
+ span {
+ align-self: flex-start;
+ margin-top: 5px;
+ cursor: pointer;
+ }
+`;
+
+type RefRectParams = {
+ // body params
+ bh: number;
+ bw: number;
+ // target params
+ th: number;
+ tw: number;
+ tx: number;
+ ty: number;
+};
+
+/*
+ * Clip Path Polygon :
+ * 1) 0 0 ----> (body start) (body start)
+ * 2) 0 ${boundingRect.bh} ----> (body start) (body end)
+ * 3) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
+ * 4) ${boundingRect.tx} ${boundingRect.ty} ----> (target start) (target start)
+ * 5) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty} ----> (target end) (target start)
+ * 6) ${boundingRect.tx + boundingRect.tw} ${boundingRect.ty + boundingRect.th} ----> (target end) (target end)
+ * 7) ${boundingRect.tx} ${boundingRect.ty + boundingRect.th} ----> (target start) (target end)
+ * 8) ${boundingRect.tx} ${boundingRect.bh} ----> (target start) (body end)
+ * 9) ${boundingRect.bw} ${boundingRect.bh} ----> (body end) (body end)
+ * 10) ${boundingRect.bw} 0 ----> (body end) (body start)
+ *
+ *
+ * 1 ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ←10
+ * ↓ ↑
+ * ↓ Body ↑
+ * ↓ ↑
+ * ↓ ↑
+ * ↓ 4 → → → → → → → 5 ↑
+ * ↓ ↑ / / / / / / / ↓ ↑
+ * ↓ ↑ / / /Target/ /↓ ↑
+ * ↓ ↑ / / / / / / / ↓ ↑
+ * ↓ 7 ← ← ← ← ← ← ← 6 ↑
+ * ↓ ↑↓ ↑
+ * ↓ ↑↓ ↑
+ * 2 → → → → 3,8 → → → → → → → → → → → 9
+ */
+
+/**
+ * Creates a Highlighting Clipping mask around a target container
+ * @param targetId Id for the target container to show highlighting around it
+ */
+
+const WalkthroughRenderer = ({
+ details,
+ offset,
+ onDismiss,
+ targetId,
+ eventParams = {},
+}: FeatureParams) => {
+ const [boundingRect, setBoundingRect] = useState(null);
+ const { popFeature } = useContext(WalkthroughContext) || {};
+ const updateBoundingRect = () => {
+ const highlightArea = document.querySelector(`#${targetId}`);
+ if (highlightArea) {
+ const boundingRect = highlightArea.getBoundingClientRect();
+ const bodyRect = document.body.getBoundingClientRect();
+ const offsetHighlightPad =
+ typeof offset?.highlightPad === "number"
+ ? offset?.highlightPad
+ : PADDING_HIGHLIGHT;
+ setBoundingRect({
+ bw: bodyRect.width,
+ bh: bodyRect.height,
+ tw: boundingRect.width + 2 * offsetHighlightPad,
+ th: boundingRect.height + 2 * offsetHighlightPad,
+ tx: boundingRect.x - offsetHighlightPad,
+ ty: boundingRect.y - offsetHighlightPad,
+ });
+ showIndicator(`#${targetId}`, offset?.position, {
+ top: offset?.indicatorTop || 0,
+ left: offset?.indicatorLeft || 0,
+ zIndex: Z_INDEX + 1,
+ });
+ }
+ };
+
+ useEffect(() => {
+ updateBoundingRect();
+ const highlightArea = document.querySelector(`#${targetId}`);
+ AnalyticsUtil.logEvent("WALKTHROUGH_SHOWN", eventParams);
+ window.addEventListener("resize", updateBoundingRect);
+ const resizeObserver = new ResizeObserver(updateBoundingRect);
+ if (highlightArea) {
+ resizeObserver.observe(highlightArea);
+ }
+ return () => {
+ window.removeEventListener("resize", updateBoundingRect);
+ if (highlightArea) resizeObserver.unobserve(highlightArea);
+ };
+ }, [targetId]);
+
+ const onDismissWalkthrough = () => {
+ onDismiss && onDismiss();
+ popFeature && popFeature();
+ };
+
+ if (!boundingRect) return null;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const InstructionsComponent = ({
+ details,
+ offset,
+ onClose,
+ targetId,
+}: {
+ details?: FeatureDetails;
+ offset?: OffsetType;
+ targetId: string;
+ onClose: () => void;
+}) => {
+ if (!details) return null;
+
+ const positionAttr = getPosition({
+ targetId,
+ offset,
+ });
+
+ return (
+
+
+
+ {details.title}
+
+
+
+ {details.description}
+ {details.imageURL && (
+
+
+
+ )}
+
+ );
+};
+
+export default WalkthroughRenderer;
diff --git a/app/client/src/components/formControls/WhereClauseControl.tsx b/app/client/src/components/formControls/WhereClauseControl.tsx
index 99008248c6..9e5f622fdc 100644
--- a/app/client/src/components/formControls/WhereClauseControl.tsx
+++ b/app/client/src/components/formControls/WhereClauseControl.tsx
@@ -282,7 +282,7 @@ function ConditionComponent(props: any, index: number) {
props.onDeletePressed(index);
}}
size="md"
- startIcon="cross-line"
+ startIcon="close"
/>
);
@@ -397,7 +397,7 @@ function ConditionBlock(props: any) {
onDeletePressed(index);
}}
size="md"
- startIcon="cross-line"
+ startIcon="close"
top={"24px"}
/>
diff --git a/app/client/src/constants/Datasource.ts b/app/client/src/constants/Datasource.ts
index 3cb14ebb6b..4228996469 100644
--- a/app/client/src/constants/Datasource.ts
+++ b/app/client/src/constants/Datasource.ts
@@ -24,6 +24,7 @@ export const DatasourceCreateEntryPoints = {
export const DatasourceEditEntryPoints = {
DATASOURCE_CARD_EDIT: "DATASOURCE_CARD_EDIT",
DATASOURCE_FORM_EDIT: "DATASOURCE_FORM_EDIT",
+ QUERY_EDITOR_DATASOURCE_SCHEMA: "QUERY_EDITOR_DATASOURCE_SCHEMA",
};
export const DB_QUERY_DEFAULT_TABLE_NAME = "<>";
diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx
index b456935216..a00e9542f6 100644
--- a/app/client/src/constants/DefaultTheme.tsx
+++ b/app/client/src/constants/DefaultTheme.tsx
@@ -3015,7 +3015,7 @@ export const theme: Theme = {
},
},
actionSidePane: {
- width: 265,
+ width: 280,
},
onboarding: {
statusBarHeight: 92,
diff --git a/app/client/src/entities/Action/index.ts b/app/client/src/entities/Action/index.ts
index 558a9c1742..faef9e3fb9 100644
--- a/app/client/src/entities/Action/index.ts
+++ b/app/client/src/entities/Action/index.ts
@@ -40,6 +40,7 @@ export enum PluginName {
SNOWFLAKE = "Snowflake",
ARANGODB = "ArangoDB",
REDSHIFT = "Redshift",
+ SMTP = "SMTP",
}
export enum PaginationType {
diff --git a/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx b/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx
index 48ce5ac333..fc3908231a 100644
--- a/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx
+++ b/app/client/src/pages/Editor/APIEditor/ApiRightPane.tsx
@@ -15,6 +15,7 @@ import { useDispatch, useSelector } from "react-redux";
import { getApiRightPaneSelectedTab } from "selectors/apiPaneSelectors";
import isUndefined from "lodash/isUndefined";
import { Button, Tab, TabPanel, Tabs, TabsList, Tag } from "design-system";
+import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer";
import type { Datasource } from "entities/Datasource";
import { getCurrentEnvironment } from "@appsmith/utils/Environments";
@@ -125,7 +126,7 @@ const DataSourceNameContainer = styled.div`
const SomeWrapper = styled.div`
height: 100%;
- padding: 0 var(--ads-v2-spaces-6);
+ padding: 0 var(--ads-v2-spaces-4);
`;
const NoEntityFoundWrapper = styled.div`
@@ -311,9 +312,12 @@ function ApiRightPane(props: any) {
diff --git a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx
index 5ec1d3ba8d..e123b6278d 100644
--- a/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx
+++ b/app/client/src/pages/Editor/APIEditor/CommonEditorForm.tsx
@@ -735,9 +735,11 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
applicationId={props.applicationId}
currentActionDatasourceId={currentActionDatasourceId}
currentPageId={props.currentPageId}
+ datasourceId={props.currentActionDatasourceId}
datasources={props.datasources}
hasResponse={props.hasResponse}
onClick={updateDatasource}
+ pluginId={props.pluginId}
suggestedWidgets={props.suggestedWidgets}
/>
diff --git a/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx b/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx
index cc03bc7eb3..607977e416 100644
--- a/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx
+++ b/app/client/src/pages/Editor/AppSettingsPane/AppSettings/NavigationSettings/index.tsx
@@ -24,7 +24,7 @@ import { Spinner } from "design-system";
import LogoInput from "@appsmith/pages/Editor/NavigationSettings/LogoInput";
import SwitchSettingForLogoConfiguration from "./SwitchSettingForLogoConfiguration";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
-import { useFeatureFlagCheck } from "selectors/featureFlagsSelectors";
+import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
/**
* TODO - @Dhruvik - ImprovedAppNav
@@ -48,8 +48,8 @@ export type LogoConfigurationSwitches = {
function NavigationSettings() {
const application = useSelector(getCurrentApplication);
const applicationId = useSelector(getCurrentApplicationId);
- const isAppLogoEnabled = useFeatureFlagCheck(
- FEATURE_FLAG.APP_NAVIGATION_LOGO_UPLOAD,
+ const isAppLogoEnabled = useSelector((state) =>
+ selectFeatureFlagCheck(state, FEATURE_FLAG.APP_NAVIGATION_LOGO_UPLOAD),
);
const dispatch = useDispatch();
const [navigationSetting, setNavigationSetting] = useState(
diff --git a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx
index 79c083396f..21bc0fe54d 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/DBForm.tsx
@@ -38,12 +38,13 @@ type Props = DatasourceDBEditorProps &
export const Form = styled.form<{
showFilterComponent: boolean;
+ viewMode: boolean;
}>`
display: flex;
flex-direction: column;
- height: ${({ theme }) => `calc(100% - ${theme.backBanner})`};
+ ${(props) =>
+ !props.viewMode && `height: ${`calc(100% - ${props?.theme.backBanner})`};`}
overflow-y: scroll;
- flex: 8 8 80%;
padding-bottom: 20px;
margin-left: ${(props) => (props.showFilterComponent ? "24px" : "0px")};
`;
@@ -90,6 +91,7 @@ class DatasourceDBEditor extends JSONtoForm {
e.preventDefault();
}}
showFilterComponent={showFilterComponent}
+ viewMode={viewMode}
>
{messages &&
messages.map((msg, i) => {
diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
index 71206ca1ea..ebf309e138 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
@@ -72,6 +72,7 @@ interface DatasourceRestApiEditorProps {
toggleSaveActionFlag: (flag: boolean) => void;
triggerSave?: boolean;
datasourceDeleteTrigger: () => void;
+ viewMode: boolean;
}
type Props = DatasourceRestApiEditorProps &
@@ -247,6 +248,7 @@ class DatasourceRestAPIEditor extends React.Component {
e.preventDefault();
}}
showFilterComponent={this.props.showFilterComponent}
+ viewMode={this.props.viewMode}
>
{this.renderEditor()}
diff --git a/app/client/src/pages/Editor/DataSourceEditor/index.tsx b/app/client/src/pages/Editor/DataSourceEditor/index.tsx
index 34881681b9..36050495bd 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/index.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/index.tsx
@@ -32,7 +32,6 @@ import type { RouteComponentProps } from "react-router";
import EntityNotFoundPane from "pages/Editor/EntityNotFoundPane";
import { DatasourceComponentTypes } from "api/PluginApi";
import DatasourceSaasForm from "../SaaSEditor/DatasourceForm";
-
import {
getCurrentApplicationId,
getPagePermissions,
@@ -493,51 +492,49 @@ class DatasourceEditorRouter extends React.Component {
} = this.props;
const shouldViewMode = viewMode && !isInsideReconnectModal;
- // Check for specific form types first
- if (
- pluginDatasourceForm === DatasourceComponentTypes.RestAPIDatasourceForm &&
- !shouldViewMode
- ) {
- return (
- <>
-
- {this.renderSaveDisacardModal()}
- >
- );
- }
- // Default to DB Editor Form
return (
<>
-
+ {
+ // Check for specific form types first
+ pluginDatasourceForm ===
+ DatasourceComponentTypes.RestAPIDatasourceForm &&
+ !shouldViewMode ? (
+
+ ) : (
+ // Default to DB Editor Form
+
+ )
+ }
{this.renderSaveDisacardModal()}
>
);
diff --git a/app/client/src/pages/Editor/Explorer/Datasources.tsx b/app/client/src/pages/Editor/Explorer/Datasources.tsx
index b4741526be..fbdf4adb9e 100644
--- a/app/client/src/pages/Editor/Explorer/Datasources.tsx
+++ b/app/client/src/pages/Editor/Explorer/Datasources.tsx
@@ -123,11 +123,11 @@ const Datasources = React.memo(() => {
{
- dispatch(refreshDatasourceStructure(props.datasourceId));
+ dispatch(
+ refreshDatasourceStructure(
+ props.datasourceId,
+ DatasourceStructureContext.EXPLORER,
+ ),
+ );
}, [dispatch, props.datasourceId]);
const [confirmDelete, setConfirmDelete] = useState(false);
diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx
index d18e39cf78..c6616c49f6 100644
--- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx
+++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceEntity.tsx
@@ -13,9 +13,16 @@ import {
} from "actions/datasourceActions";
import { useDispatch, useSelector } from "react-redux";
import type { AppState } from "@appsmith/reducers";
-import { DatasourceStructureContainer } from "./DatasourceStructureContainer";
+import {
+ DatasourceStructureContainer,
+ DatasourceStructureContext,
+} from "./DatasourceStructureContainer";
import { isStoredDatasource, PluginType } from "entities/Action";
-import { getAction } from "selectors/entitiesSelector";
+import {
+ getAction,
+ getDatasourceStructureById,
+ getIsFetchingDatasourceStructure,
+} from "selectors/entitiesSelector";
import {
datasourcesEditorIdURL,
saasEditorDatasourceIdURL,
@@ -81,13 +88,13 @@ const ExplorerDatasourceEntity = React.memo(
const updateDatasourceNameCall = (id: string, name: string) =>
updateDatasourceName({ id: props.datasource.id, name });
- const datasourceStructure = useSelector((state: AppState) => {
- return state.entities.datasources.structure[props.datasource.id];
- });
+ const datasourceStructure = useSelector((state: AppState) =>
+ getDatasourceStructureById(state, props.datasource.id),
+ );
- const isFetchingDatasourceStructure = useSelector((state: AppState) => {
- return state.entities.datasources.fetchingDatasourceStructure;
- });
+ const isFetchingDatasourceStructure = useSelector((state: AppState) =>
+ getIsFetchingDatasourceStructure(state, props.datasource.id),
+ );
const expandDatasourceId = useSelector((state: AppState) => {
return state.ui.datasourcePane.expandDatasourceId;
@@ -95,7 +102,13 @@ const ExplorerDatasourceEntity = React.memo(
//Debounce fetchDatasourceStructure request.
const debounceFetchDatasourceRequest = debounce(async () => {
- dispatch(fetchDatasourceStructure(props.datasource.id, true));
+ dispatch(
+ fetchDatasourceStructure(
+ props.datasource.id,
+ true,
+ DatasourceStructureContext.EXPLORER,
+ ),
+ );
}, 300);
const getDatasourceStructure = useCallback(
@@ -155,6 +168,7 @@ const ExplorerDatasourceEntity = React.memo(
updateEntityName={updateDatasourceNameCall}
>
setActive(false));
+ const { isOpened: isWalkthroughOpened, popFeature } =
+ useContext(WalkthroughContext) || {};
+
const datasource = useSelector((state: AppState) =>
getDatasource(state, props.datasourceId),
);
+ const plugin: Plugin | undefined = useSelector((state) =>
+ getPlugin(state, datasource?.pluginId || ""),
+ );
+
const datasourcePermissions = datasource?.userPermissions || [];
const pagePermissions = useSelector(getPagePermissions);
@@ -44,62 +61,98 @@ export function DatasourceStructure(props: DatasourceStructureProps) {
...pagePermissions,
]);
- const lightningMenu = canCreateDatasourceActions ? (
-
- ) : null;
+
+
+ ) : null;
if (dbStructure.templates) templateMenu = lightningMenu;
const columnsAndKeys = dbStructure.columns.concat(dbStructure.keys);
return (
canCreateDatasourceActions && setActive(!active)}
+ action={onEntityClick}
active={active}
- className={`datasourceStructure`}
+ className={`datasourceStructure${
+ props.context !== DatasourceStructureContext.EXPLORER &&
+ `-${props.context}`
+ }`}
contextMenu={templateMenu}
- entityId={"DatasourceStructure"}
+ entityId={`${props.datasourceId}-${dbStructure.name}-${props.context}`}
+ forceExpand={props.forceExpand}
icon={datasourceTableIcon}
+ isDefaultExpanded={props?.isDefaultOpen}
name={dbStructure.name}
step={props.step}
>
- {columnsAndKeys.map((field, index) => {
- return (
-
- );
- })}
+ <>
+ {columnsAndKeys.map((field, index) => {
+ return (
+
+ );
+ })}
+ >
);
}
diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx
index 955f7c218b..9a69b4cbe4 100644
--- a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx
+++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureContainer.tsx
@@ -1,57 +1,205 @@
import {
createMessage,
+ DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT,
SCHEMA_NOT_AVAILABLE,
+ TABLE_OR_COLUMN_NOT_FOUND,
} from "@appsmith/constants/messages";
import type {
DatasourceStructure as DatasourceStructureType,
DatasourceTable,
} from "entities/Datasource";
import type { ReactElement } from "react";
-import React, { memo } from "react";
+import React, { memo, useEffect, useMemo, useState } from "react";
import EntityPlaceholder from "../Entity/Placeholder";
-import { useEntityUpdateState } from "../hooks";
import DatasourceStructure from "./DatasourceStructure";
+import { Input, Text } from "design-system";
+import styled from "styled-components";
+import { getIsFetchingDatasourceStructure } from "selectors/entitiesSelector";
+import { useSelector } from "react-redux";
+import type { AppState } from "@appsmith/reducers";
+import DatasourceStructureLoadingContainer from "./DatasourceStructureLoadingContainer";
+import DatasourceStructureNotFound from "./DatasourceStructureNotFound";
+import AnalyticsUtil from "utils/AnalyticsUtil";
type Props = {
datasourceId: string;
datasourceStructure?: DatasourceStructureType;
step: number;
+ context: DatasourceStructureContext;
+ pluginName?: string;
+ currentActionId?: string;
};
+export enum DatasourceStructureContext {
+ EXPLORER = "entity-explorer",
+ QUERY_EDITOR = "query-editor",
+ // this does not exist yet, but in case it does in the future.
+ API_EDITOR = "api-editor",
+}
+
+const DatasourceStructureSearchContainer = styled.div`
+ margin-bottom: 8px;
+ position: sticky;
+ top: 0;
+ overflow: hidden;
+ z-index: 10;
+ background: white;
+`;
+
const Container = (props: Props) => {
- const isLoading = useEntityUpdateState(props.datasourceId);
- let view: ReactElement = ;
+ const isLoading = useSelector((state: AppState) =>
+ getIsFetchingDatasourceStructure(state, props.datasourceId),
+ );
+ let view: ReactElement | JSX.Element = ;
+
+ const [datasourceStructure, setDatasourceStructure] = useState<
+ DatasourceStructureType | undefined
+ >(props.datasourceStructure);
+ const [hasSearchedOccured, setHasSearchedOccured] = useState(false);
+
+ useEffect(() => {
+ if (datasourceStructure !== props.datasourceStructure) {
+ setDatasourceStructure(props.datasourceStructure);
+ }
+ }, [props.datasourceStructure]);
+
+ const flatStructure = useMemo(() => {
+ if (!props.datasourceStructure?.tables?.length) return [];
+ const list: string[] = [];
+
+ props.datasourceStructure.tables.map((table) => {
+ table.columns.forEach((column) => {
+ list.push(`${table.name}~${column.name}`);
+ });
+ });
+
+ return list;
+ }, [props.datasourceStructure]);
+
+ const handleOnChange = (value: string) => {
+ if (!props.datasourceStructure?.tables?.length) return;
+
+ if (value.length > 0) {
+ !hasSearchedOccured && setHasSearchedOccured(true);
+ } else {
+ hasSearchedOccured && setHasSearchedOccured(false);
+ }
+
+ const tables = new Set();
+ const columns = new Set();
+
+ flatStructure.forEach((structure) => {
+ const segments = structure.split("~");
+ // if the value is present in the columns, add the column and its parent table.
+ if (segments[1].toLowerCase().includes(value)) {
+ tables.add(segments[0]);
+ columns.add(segments[1]);
+ return;
+ }
+
+ // if the value is present in the table but not in the columns, add the table
+ if (segments[0].toLowerCase().includes(value)) {
+ tables.add(segments[0]);
+ return;
+ }
+ });
+
+ const filteredDastasourceStructure = props.datasourceStructure.tables
+ .map((structure) => ({
+ ...structure,
+ columns:
+ // if the size of the columns set is 0, then simply default to the entire column
+ columns.size === 0
+ ? structure.columns
+ : structure.columns.filter((column) => columns.has(column.name)),
+ keys:
+ columns.size === 0
+ ? structure.keys
+ : structure.keys.filter((key) => columns.has(key.name)),
+ }))
+ .filter((table) => tables.has(table.name));
+
+ setDatasourceStructure({ tables: filteredDastasourceStructure });
+
+ AnalyticsUtil.logEvent("DATASOURCE_SCHEMA_SEARCH", {
+ datasourceId: props.datasourceId,
+ pluginName: props.pluginName,
+ });
+ };
if (!isLoading) {
if (props.datasourceStructure?.tables?.length) {
view = (
<>
- {props.datasourceStructure.tables.map(
- (structure: DatasourceTable) => {
+ {props.context !== DatasourceStructureContext.EXPLORER && (
+
+ handleOnChange(value)}
+ placeholder={createMessage(
+ DATASOURCE_STRUCTURE_INPUT_PLACEHOLDER_TEXT,
+ )}
+ size={"md"}
+ startIcon="search"
+ type="text"
+ />
+
+ )}
+ {!!datasourceStructure?.tables?.length &&
+ datasourceStructure.tables.map((structure: DatasourceTable) => {
return (
);
- },
+ })}
+
+ {!datasourceStructure?.tables?.length && (
+
+ {createMessage(TABLE_OR_COLUMN_NOT_FOUND)}
+
)}
>
);
} else {
- view = (
-
- {props.datasourceStructure &&
- props.datasourceStructure.error &&
- props.datasourceStructure.error.message &&
- props.datasourceStructure.error.message !== "null"
- ? props.datasourceStructure.error.message
- : createMessage(SCHEMA_NOT_AVAILABLE)}
-
- );
+ if (props.context !== DatasourceStructureContext.EXPLORER) {
+ view = (
+
+ );
+ } else {
+ view = (
+
+ {props.datasourceStructure &&
+ props.datasourceStructure.error &&
+ props.datasourceStructure.error.message &&
+ props.datasourceStructure.error.message !== "null"
+ ? props.datasourceStructure.error.message
+ : createMessage(SCHEMA_NOT_AVAILABLE)}
+
+ );
+ }
}
+ } else if (
+ // intentionally leaving this here in case we want to show loading states in the explorer or query editor page
+ props.context !== DatasourceStructureContext.EXPLORER &&
+ isLoading
+ ) {
+ view = ;
}
return view;
diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureHeader.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureHeader.tsx
new file mode 100644
index 0000000000..bc3fa6d089
--- /dev/null
+++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureHeader.tsx
@@ -0,0 +1,46 @@
+import React, { useCallback } from "react";
+import { useDispatch } from "react-redux";
+import { Icon, Text } from "design-system";
+import styled from "styled-components";
+import { refreshDatasourceStructure } from "actions/datasourceActions";
+import { SCHEMA_LABEL, createMessage } from "@appsmith/constants/messages";
+import { DatasourceStructureContext } from "./DatasourceStructureContainer";
+
+type Props = {
+ datasourceId: string;
+};
+
+const HeaderWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+`;
+
+export default function DatasourceStructureHeader(props: Props) {
+ const dispatch = useDispatch();
+
+ const dispatchRefresh = useCallback(
+ (event: React.MouseEvent) => {
+ event.stopPropagation();
+ dispatch(
+ refreshDatasourceStructure(
+ props.datasourceId,
+ DatasourceStructureContext.QUERY_EDITOR,
+ ),
+ );
+ },
+ [dispatch, props.datasourceId],
+ );
+
+ return (
+
+
+ {createMessage(SCHEMA_LABEL)}
+
+ dispatchRefresh(event)}>
+
+
+
+ );
+}
diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureLoadingContainer.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureLoadingContainer.tsx
new file mode 100644
index 0000000000..4b81c7bf8e
--- /dev/null
+++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureLoadingContainer.tsx
@@ -0,0 +1,36 @@
+import React from "react";
+import { createMessage, LOADING_SCHEMA } from "@appsmith/constants/messages";
+import { Spinner, Text } from "design-system";
+import styled from "styled-components";
+
+const LoadingContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+
+ & > p {
+ margin-left: 0.5rem;
+ }
+`;
+
+const SpinnerWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
+
+const DatasourceStructureLoadingContainer = () => {
+ return (
+
+
+
+
+
+ {createMessage(LOADING_SCHEMA)}
+
+
+ );
+};
+
+export default DatasourceStructureLoadingContainer;
diff --git a/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureNotFound.tsx b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureNotFound.tsx
new file mode 100644
index 0000000000..607d456e42
--- /dev/null
+++ b/app/client/src/pages/Editor/Explorer/Datasources/DatasourceStructureNotFound.tsx
@@ -0,0 +1,73 @@
+import React from "react";
+import { useSelector } from "react-redux";
+import styled from "styled-components";
+import { Text, Button } from "design-system";
+import type { APIResponseError } from "api/ApiResponses";
+import { EDIT_DATASOURCE, createMessage } from "@appsmith/constants/messages";
+import AnalyticsUtil from "utils/AnalyticsUtil";
+import { DatasourceEditEntryPoints } from "constants/Datasource";
+import history from "utils/history";
+import { getQueryParams } from "utils/URLUtils";
+import { datasourcesEditorIdURL } from "RouteBuilder";
+import { omit } from "lodash";
+import { getCurrentPageId } from "selectors/editorSelectors";
+
+export type Props = {
+ error: APIResponseError | { message: string } | undefined;
+ datasourceId: string;
+ pluginName?: string;
+};
+
+const NotFoundContainer = styled.div`
+ display: flex;
+ height: 100%;
+ width: 100%;
+ flex-direction: column;
+`;
+
+const NotFoundText = styled(Text)`
+ margin-bottom: 1rem;
+ margin-top: 0.3rem;
+`;
+
+const ButtonWrapper = styled.div`
+ width: fit-content;
+`;
+
+const DatasourceStructureNotFound = (props: Props) => {
+ const { datasourceId, error, pluginName } = props;
+
+ const pageId = useSelector(getCurrentPageId);
+
+ const editDatasource = () => {
+ AnalyticsUtil.logEvent("EDIT_DATASOURCE_CLICK", {
+ datasourceId: datasourceId,
+ pluginName: pluginName,
+ entryPoint: DatasourceEditEntryPoints.QUERY_EDITOR_DATASOURCE_SCHEMA,
+ });
+
+ const url = datasourcesEditorIdURL({
+ pageId,
+ datasourceId: datasourceId,
+ params: { ...omit(getQueryParams(), "viewMode"), viewMode: false },
+ });
+ history.push(url);
+ };
+
+ return (
+
+ {error?.message && (
+
+ {error.message}
+
+ )}
+
+
+
+
+ );
+};
+
+export default DatasourceStructureNotFound;
diff --git a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx
index 4f01fc0de8..1450313194 100644
--- a/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx
+++ b/app/client/src/pages/Editor/Explorer/Datasources/QueryTemplates.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useContext } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createActionRequest } from "actions/pluginActionActions";
import type { AppState } from "@appsmith/reducers";
@@ -11,25 +11,66 @@ import type { QueryAction } from "entities/Action";
import history from "utils/history";
import type { Datasource, QueryTemplate } from "entities/Datasource";
import { INTEGRATION_TABS } from "constants/routes";
-import { getDatasource, getPlugin } from "selectors/entitiesSelector";
+import {
+ getAction,
+ getDatasource,
+ getPlugin,
+} from "selectors/entitiesSelector";
import { integrationEditorURL } from "RouteBuilder";
import { MenuItem } from "design-system";
import type { Plugin } from "api/PluginApi";
+import { DatasourceStructureContext } from "./DatasourceStructureContainer";
+import WalkthroughContext from "components/featureWalkthrough/walkthroughContext";
+import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
+import { setFeatureFlagShownStatus } from "utils/storage";
+import styled from "styled-components";
+import { change, getFormValues } from "redux-form";
+import { QUERY_EDITOR_FORM_NAME } from "@appsmith/constants/forms";
+import { diff } from "deep-diff";
+import { UndoRedoToastContext, showUndoRedoToast } from "utils/replayHelpers";
+import AnalyticsUtil from "utils/AnalyticsUtil";
type QueryTemplatesProps = {
templates: QueryTemplate[];
datasourceId: string;
onSelect: () => void;
+ context: DatasourceStructureContext;
+ currentActionId: string;
};
+enum QueryTemplatesEvent {
+ EXPLORER_TEMPLATE = "explorer-template",
+ QUERY_EDITOR_TEMPLATE = "query-editor-template",
+}
+
+const TemplateMenuItem = styled(MenuItem)`
+ & > span {
+ text-transform: lowercase;
+ }
+
+ & > span:first-letter {
+ text-transform: capitalize;
+ }
+`;
+
export function QueryTemplates(props: QueryTemplatesProps) {
const dispatch = useDispatch();
+ const { isOpened: isWalkthroughOpened, popFeature } =
+ useContext(WalkthroughContext) || {};
const applicationId = useSelector(getCurrentApplicationId);
const actions = useSelector((state: AppState) => state.entities.actions);
const currentPageId = useSelector(getCurrentPageId);
const dataSource: Datasource | undefined = useSelector((state: AppState) =>
getDatasource(state, props.datasourceId),
);
+
+ const currentAction = useSelector((state) =>
+ getAction(state, props.currentActionId),
+ );
+ const formName = QUERY_EDITOR_FORM_NAME;
+
+ const formValues = useSelector((state) => getFormValues(formName)(state));
+
const plugin: Plugin | undefined = useSelector((state: AppState) =>
getPlugin(state, !!dataSource?.pluginId ? dataSource.pluginId : ""),
);
@@ -55,14 +96,24 @@ export function QueryTemplates(props: QueryTemplatesProps) {
},
eventData: {
actionType: "Query",
- from: "explorer-template",
+ from:
+ props?.context === DatasourceStructureContext.EXPLORER
+ ? QueryTemplatesEvent.EXPLORER_TEMPLATE
+ : QueryTemplatesEvent.QUERY_EDITOR_TEMPLATE,
dataSource: dataSource?.name,
datasourceId: props.datasourceId,
pluginName: plugin?.name,
+ queryType: template.title,
},
...queryactionConfiguration,
}),
);
+
+ if (isWalkthroughOpened) {
+ popFeature && popFeature();
+ setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
+ }
+
history.push(
integrationEditorURL({
pageId: currentPageId,
@@ -80,19 +131,84 @@ export function QueryTemplates(props: QueryTemplatesProps) {
],
);
+ const updateQueryAction = useCallback(
+ (template: QueryTemplate) => {
+ if (!currentAction) return;
+
+ const queryactionConfiguration: Partial = {
+ actionConfiguration: {
+ body: template.body,
+ pluginSpecifiedTemplates: template.pluginSpecifiedTemplates,
+ formData: template.configuration,
+ ...template.actionConfiguration,
+ },
+ };
+
+ const newFormValueState = {
+ ...formValues,
+ ...queryactionConfiguration,
+ };
+
+ const differences = diff(formValues, newFormValueState) || [];
+
+ differences.forEach((diff) => {
+ if (diff.kind === "E" || diff.kind === "N") {
+ const path = diff?.path?.join(".") || "";
+ const value = diff?.rhs;
+
+ if (path) {
+ dispatch(change(QUERY_EDITOR_FORM_NAME, path, value));
+ }
+ }
+ });
+
+ AnalyticsUtil.logEvent("AUTOMATIC_QUERY_GENERATION", {
+ datasourceId: props.datasourceId,
+ pluginName: plugin?.name || "",
+ templateCommand: template?.title,
+ isWalkthroughOpened,
+ });
+
+ if (isWalkthroughOpened) {
+ popFeature && popFeature();
+ setFeatureFlagShownStatus(FEATURE_FLAG.ab_ds_schema_enabled, true);
+ }
+
+ showUndoRedoToast(
+ currentAction.name,
+ false,
+ false,
+ true,
+ UndoRedoToastContext.QUERY_TEMPLATES,
+ );
+ },
+ [
+ dispatch,
+ actions,
+ currentPageId,
+ applicationId,
+ props.datasourceId,
+ dataSource,
+ ],
+ );
+
return (
<>
{props.templates.map((template) => {
return (
-
+
);
})}
>
diff --git a/app/client/src/pages/Editor/Explorer/Entity/index.tsx b/app/client/src/pages/Editor/Explorer/Entity/index.tsx
index d302f48aca..4dced47790 100644
--- a/app/client/src/pages/Editor/Explorer/Entity/index.tsx
+++ b/app/client/src/pages/Editor/Explorer/Entity/index.tsx
@@ -255,7 +255,7 @@ export type EntityProps = {
export const Entity = forwardRef(
(props: EntityProps, ref: React.Ref) => {
const isEntityOpen = useSelector((state: AppState) =>
- getEntityCollapsibleState(state, props.name),
+ getEntityCollapsibleState(state, props.entityId),
);
const isDefaultExpanded = useMemo(() => !!props.isDefaultExpanded, []);
const { canEditEntityName = false, showAddButton = false } = props;
@@ -270,7 +270,7 @@ export const Entity = forwardRef(
const open = (shouldOpen: boolean | undefined) => {
if (!!props.children && props.name && isOpen !== shouldOpen) {
- dispatch(setEntityCollapsibleState(props.name, !!shouldOpen));
+ dispatch(setEntityCollapsibleState(props.entityId, !!shouldOpen));
}
};
diff --git a/app/client/src/pages/Editor/Explorer/Files/index.tsx b/app/client/src/pages/Editor/Explorer/Files/index.tsx
index 8128823b93..7199240b8a 100644
--- a/app/client/src/pages/Editor/Explorer/Files/index.tsx
+++ b/app/client/src/pages/Editor/Explorer/Files/index.tsx
@@ -118,13 +118,13 @@ function Files() {
openMenu={isMenuOpen}
/>
}
- entityId={pageId + "_widgets"}
+ entityId={pageId + "_actions"}
icon={null}
isDefaultExpanded={
- isFilesOpen === null || isFilesOpen === undefined ? false : isFilesOpen
+ isFilesOpen === null || isFilesOpen === undefined ? true : isFilesOpen
}
isSticky
- key={pageId + "_widgets"}
+ key={pageId + "_actions"}
name="Queries/JS"
onCreate={onCreate}
onToggle={onFilesToggle}
diff --git a/app/client/src/pages/Editor/Explorer/Libraries/index.tsx b/app/client/src/pages/Editor/Explorer/Libraries/index.tsx
index 4b2789da67..22828a03e8 100644
--- a/app/client/src/pages/Editor/Explorer/Libraries/index.tsx
+++ b/app/client/src/pages/Editor/Explorer/Libraries/index.tsx
@@ -22,7 +22,10 @@ import {
} from "actions/JSLibraryActions";
import EntityAddButton from "../Entity/AddButton";
import type { TJSLibrary } from "workers/common/JSLibrary";
-import { getPagePermissions } from "selectors/editorSelectors";
+import {
+ getCurrentPageId,
+ getPagePermissions,
+} from "selectors/editorSelectors";
import { hasCreateActionPermission } from "@appsmith/utils/permissionHelpers";
import recommendedLibraries from "./recommendedLibraries";
import { useTransition, animated } from "react-spring";
@@ -266,6 +269,7 @@ function LibraryEntity({ lib }: { lib: TJSLibrary }) {
}
function JSDependencies() {
+ const pageId = useSelector(getCurrentPageId) || "";
const libraries = useSelector(selectLibrariesForExplorer);
const transitions = useTransition(libraries, {
keys: (lib) => lib.name,
@@ -311,7 +315,7 @@ function JSDependencies() {
}
- entityId="library_section"
+ entityId={pageId + "_library_section"}
icon={null}
isDefaultExpanded={isOpen}
isSticky
diff --git a/app/client/src/pages/Editor/Explorer/hooks.ts b/app/client/src/pages/Editor/Explorer/hooks.ts
index 364e92fe1d..3502f52e22 100644
--- a/app/client/src/pages/Editor/Explorer/hooks.ts
+++ b/app/client/src/pages/Editor/Explorer/hooks.ts
@@ -345,9 +345,8 @@ export const useFilteredEntities = (
};
export const useEntityUpdateState = (entityId: string) => {
- return useSelector(
- (state: AppState) =>
- get(state, "ui.explorer.entity.updatingEntity") === entityId,
+ return useSelector((state: AppState) =>
+ get(state, "ui.explorer.entity.updatingEntity")?.includes(entityId),
);
};
diff --git a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx
index 8d5b8f44e4..6ee2bf26c4 100644
--- a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx
+++ b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx
@@ -231,10 +231,6 @@ function GeneratePageForm() {
useState
("");
const datasourcesStructure = useSelector(getDatasourcesStructure);
- const isFetchingDatasourceStructure = useSelector(
- getIsFetchingDatasourceStructure,
- );
-
const generateCRUDSupportedPlugin: GenerateCRUDEnabledPluginMap = useSelector(
getGenerateCRUDEnabledPluginMap,
);
@@ -249,6 +245,10 @@ function GeneratePageForm() {
DEFAULT_DROPDOWN_OPTION,
);
+ const isFetchingDatasourceStructure = useSelector((state: AppState) =>
+ getIsFetchingDatasourceStructure(state, selectedDatasource.id || ""),
+ );
+
const [isSelectedTableEmpty, setIsSelectedTableEmpty] =
useState(false);
diff --git a/app/client/src/pages/Editor/GuidedTour/utils.ts b/app/client/src/pages/Editor/GuidedTour/utils.ts
index 5ce5fe4c06..685feb86d6 100644
--- a/app/client/src/pages/Editor/GuidedTour/utils.ts
+++ b/app/client/src/pages/Editor/GuidedTour/utils.ts
@@ -59,7 +59,12 @@ class IndicatorHelper {
this.indicatorWidthOffset +
"px";
} else if (position === "bottom") {
- this.indicatorWrapper.style.top = coordinates.height + offset.top + "px";
+ this.indicatorWrapper.style.top =
+ coordinates.top +
+ coordinates.height -
+ this.indicatorHeightOffset +
+ offset.top +
+ "px";
this.indicatorWrapper.style.left =
coordinates.width / 2 +
coordinates.left -
@@ -68,7 +73,7 @@ class IndicatorHelper {
"px";
} else if (position === "left") {
this.indicatorWrapper.style.top =
- coordinates.top + this.indicatorHeightOffset + offset.top + "px";
+ coordinates.top - this.indicatorHeightOffset + offset.top + "px";
this.indicatorWrapper.style.left =
coordinates.left - this.indicatorWidthOffset + offset.left + "px";
} else {
@@ -90,6 +95,7 @@ class IndicatorHelper {
offset: {
top: number;
left: number;
+ zIndex?: number;
},
) {
if (this.timerId || this.indicatorWrapper) this.destroy();
@@ -111,6 +117,9 @@ class IndicatorHelper {
loop: true,
});
+ if (offset.zIndex) {
+ this.indicatorWrapper.style.zIndex = `${offset.zIndex}`;
+ }
// This is to invoke at the start and then recalculate every 3 seconds
// 3 seconds is an arbitrary value here to avoid calling getBoundingClientRect to many times
this.calculate(primaryReference, position, offset);
@@ -237,7 +246,7 @@ export function highlightSection(
export function showIndicator(
selector: string,
position = "right",
- offset = { top: 0, left: 0 },
+ offset: { top: number; left: number; zIndex?: number } = { top: 0, left: 0 },
) {
let primaryReference: Element | null = null;
diff --git a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
index e27ccf6a6e..1516d1d0fb 100644
--- a/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
+++ b/app/client/src/pages/Editor/QueryEditor/EditorJSONtoForm.tsx
@@ -9,7 +9,12 @@ import {
getPluginNameFromId,
} from "selectors/entitiesSelector";
import FormControl from "../FormControl";
-import type { Action, QueryAction, SaaSAction } from "entities/Action";
+import {
+ PluginName,
+ type Action,
+ type QueryAction,
+ type SaaSAction,
+} from "entities/Action";
import { useDispatch, useSelector } from "react-redux";
import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
@@ -126,6 +131,9 @@ import { ENTITY_TYPE as SOURCE_ENTITY_TYPE } from "entities/AppsmithConsole";
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
import ActionExecutionInProgressView from "components/editorComponents/ActionExecutionInProgressView";
import { CloseDebugger } from "components/editorComponents/Debugger/DebuggerTabs";
+import { DatasourceStructureContext } from "../Explorer/Datasources/DatasourceStructureContainer";
+import { selectFeatureFlagCheck } from "selectors/featureFlagsSelectors";
+import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
const QueryFormContainer = styled.form`
flex: 1;
@@ -304,12 +312,12 @@ const DocumentationButton = styled(Button)`
const SidebarWrapper = styled.div<{ show: boolean }>`
border-left: 1px solid var(--ads-v2-color-border);
- padding: 0 var(--ads-v2-spaces-7) var(--ads-v2-spaces-7);
- overflow: auto;
+ padding: 0 var(--ads-v2-spaces-4) var(--ads-v2-spaces-4);
+ overflow: hidden;
border-bottom: 0;
display: ${(props) => (props.show ? "flex" : "none")};
width: ${(props) => props.theme.actionSidePane.width}px;
- margin-top: 38px;
+ margin-top: 10px;
/* margin-left: var(--ads-v2-spaces-7); */
`;
@@ -354,6 +362,7 @@ type QueryFormProps = {
id,
value,
}: UpdateActionPropertyActionPayload) => void;
+ datasourceId: string;
};
type ReduxProps = {
@@ -870,6 +879,23 @@ export function EditorJSONtoForm(props: Props) {
//TODO: move this to a common place
const onClose = () => dispatch(showDebugger(false));
+ // A/B feature flag for datasource structure.
+ const isEnabledForDSSchema = useSelector((state) =>
+ selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_schema_enabled),
+ );
+
+ // A/B feature flag for query binding.
+ const isEnabledForQueryBinding = useSelector((state) =>
+ selectFeatureFlagCheck(state, FEATURE_FLAG.ab_ds_binding_enabled),
+ );
+
+ // here we check for normal conditions for opening action pane
+ // or if any of the flags are true, We should open the actionpane by default.
+ const shouldOpenActionPaneByDefault =
+ ((hasDependencies || !!output) && !guidedTourEnabled) ||
+ ((isEnabledForDSSchema || isEnabledForQueryBinding) &&
+ currentActionPluginName !== PluginName.SMTP);
+
// when switching between different redux forms, make sure this redux form has been initialized before rendering anything.
// the initialized prop below comes from redux-form.
if (!props.initialized) {
@@ -1070,14 +1096,15 @@ export function EditorJSONtoForm(props: Props) {
)}
-