[Feature] new nav sniping mode (#5746)

* added sniping mode toggle option to header

* added cover to components on hover in sniping mode

* fixed the transition time

* using filled icon

* Show dependencies in action pane

* Added a wrapper to make a widget snipeable

* removed older parts of sniping from Positioned Container

* removed onclick action from snipeable wrapper

* Showing widget name in different color

* Added a mechanism to send user to sniping mode from successful API screen

* created new property pane saga to bind the data

* Fix datasource list width issue

* Fix sidebar going out of view when the response is a table

* Minor refactor

* Show add widgets section on the sidebar

* Stop showing autocomplete option after adding a widget

* fetching pageId, appId from store

* Get suggested widget from response

* Fix table data not getting evaluated after adding binding

* Fix property pane going below the entity explorer while navigating from query/api pane

* Fix width of sidepane shifting for apis

* Fix vertical margins of connections

* Fix api pane suggested widget showing up for errors

* Fix margins

* can show select in canvas btn in sidebar

* can get the action object at the end to bind the data

* updated saga and action names

* can bind data to table

* Use themes

* Use new image url for Table widget

* Added conditional mapping for sniping mode binding.

* updated the widget name tags and seq of calls to open property pane

* pushed all sniping mode decoration to header

* moved setting sniping mode logic to editor reducer

* Added keyboard short cut to get out of sniping mode

* updated reset sniping mechanism

* removed a divider line

* if there are no relationships, will not show the complete section

* Connect Data will automatically show relevant tab in integrations

* Update list and dropdown image urls

* Remove create table button

* no wrapping bind to text

* minor review considerations

* showing the widget name to left in sniping mode

* can set data to datepicker

* will not show snipe btn if there are no widgets in canvas

* Changes for multiple suggested widgets

* removed dependency of sniping from suggested widgets

* Added analytics events for sniping mode

* logic for binding data to a widget, moved to snipeable component

* changed binding widget func from capture to onClick and took care of sniping from widget wrapper too.

* added tests to check sniping mode for table

* updated test spec

* minor fix

* Fix copy changes

* Update test to use table widget from suggested widget list

* if fails to bind will generate warning and keep user in sniping mode

* in sniping mode will only show name plate if it is under focus

* fixed the test case

* added a comment

* minor fix to capture on click event in sniping mode

* updated text

* Hide connections UI when there are no connections

* Increase width to 90%

* Show placeholder text and back button in sidepane

* Show tooltip on hover

* Add analyitcs events for suggested widgets and connections

* Update label based on whether widgets are there or not

* binding related changes

* renamed the saga file containing sinping mode sagas

* Changes for inspect entity

* Revert "binding related changes" temporarily

This reverts commit 54ae9667fecf24bc3cf9912a5356d06600b25c84.

* Update suggested widgets url

* Update table url

* Fix chart data field not getting evaluated

* a minor fix to show proper tool tip when user hovers on widget name

* Show sidepane when there is output

* Update locators

* Use constants for messages

* Update file name to ApiRightPane

* Remove delay

* Revert "Revert "binding related changes" temporarily"

This reverts commit ee7f75e83218137250b4b9a28fcf63080c185150.

* Fix width

* Fix overlap

Co-authored-by: Akash N <akash@codemonk.in>
This commit is contained in:
Pranav Kanade 2021-07-26 22:14:10 +05:30 committed by GitHub
parent 10f37da2f0
commit 3547976dc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1650 additions and 410 deletions

View File

@ -0,0 +1,27 @@
const dsl = require("../../../../fixtures/tableWidgetDsl.json");
describe("Test Create Api and Bind to Table widget", function() {
before(() => {
cy.addDsl(dsl);
});
it("Test_Add users api, execute it and go to sniping mode.", function() {
cy.createAndFillApi(this.data.userApi, "/users");
cy.RunAPI();
cy.get(".t--select-in-canvas").click();
cy.get(".t--sniping-mode-banner").should("be.visible");
});
it("Click on table name controller to bind the data and exit sniping mode", function() {
cy.get(".t--draggable-tablewidget").trigger("mouseover");
cy.get(".t--settings-sniping-control").click();
cy.get(".t--property-control-tabledata .CodeMirror").contains(
"{{Api1.data}}",
);
cy.get(".t--sniping-mode-banner").should("not.exist");
});
afterEach(() => {
// put your clean up code if any
});
});

View File

@ -11,6 +11,6 @@ describe("Inspect Entity", function() {
cy.get(".t--debugger").click();
cy.contains(".react-tabs__tab", "Inspect Entity").click();
cy.contains(".t--dependencies-item", "Button1").click();
cy.contains(".t--references-item", "Input1");
cy.contains(".t--dependencies-item", "Input1");
});
});

View File

@ -42,7 +42,7 @@ describe("Addwidget from Query and bind with other widgets", function() {
"response.body.responseMeta.status",
200,
);
cy.get(".t--add-widget").click();
cy.get(queryEditor.suggestedTableWidget).click();
cy.SearchEntityandOpen("Table1");
cy.isSelectRow(1);
cy.readTabledataPublish("1", "0").then((tabData) => {

View File

@ -31,7 +31,7 @@ describe("Add widget", function() {
"response.body.responseMeta.status",
200,
);
cy.get(".t--add-widget").click();
cy.get(queryEditor.suggestedTableWidget).click();
cy.SearchEntityandOpen("Table1");
cy.isSelectRow(1);
cy.readTabledataPublish("1", "0").then((tabData) => {

View File

@ -14,5 +14,6 @@
"queryNameField": ".t--action-name-edit-field input",
"settings": "li:contains('Settings')",
"query": "li:contains('Query')",
"switch": ".t--form-control-SWITCH input"
"switch": ".t--form-control-SWITCH input",
"suggestedTableWidget": ".t--suggested-widget-TABLE_WIDGET"
}

View File

@ -269,6 +269,17 @@ export const setActionsToExecuteOnPageLoad = (
};
};
export const bindDataOnCanvas = (payload: {
queryId: string;
applicationId: string;
pageId: string;
}) => {
return {
type: ReduxActionTypes.BIND_DATA_ON_CANVAS,
payload,
};
};
export default {
createAction: createActionRequest,
fetchActions,
@ -277,4 +288,5 @@ export default {
deleteActionSuccess,
updateAction,
updateActionSuccess,
bindDataOnCanvas,
};

View File

@ -14,3 +14,19 @@ export const hidePropertyPane = () => {
type: ReduxActionTypes.HIDE_PROPERTY_PANE,
};
};
export const bindDataToWidget = (payload: { widgetId: string }) => {
return {
type: ReduxActionTypes.BIND_DATA_TO_WIDGET,
payload,
};
};
export const setSnipingMode = (payload: boolean) => ({
type: ReduxActionTypes.SET_SNIPING_MODE,
payload,
});
export const resetSnipingMode = () => ({
type: ReduxActionTypes.RESET_SNIPING_MODE,
});

View File

@ -12,6 +12,7 @@ import { BatchAction, batchAction } from "actions/batchActions";
import PerformanceTracker, {
PerformanceTransactionName,
} from "utils/PerformanceTracker";
import { WidgetProps } from "widgets/BaseWidget";
export const executeAction = (
payload: ExecuteActionPayload,
@ -144,9 +145,9 @@ export const cutWidget = () => {
};
};
export const addTableWidgetFromQuery = (queryName: string) => {
export const addSuggestedWidget = (payload: Partial<WidgetProps>) => {
return {
type: ReduxActionTypes.ADD_TABLE_WIDGET_FROM_QUERY,
payload: queryName,
type: ReduxActionTypes.ADD_SUGGESTED_WIDGET,
payload,
};
};

View File

@ -4,6 +4,7 @@ import { DEFAULT_EXECUTE_ACTION_TIMEOUT_MS } from "constants/ApiConstants";
import axios, { AxiosPromise, CancelTokenSource } from "axios";
import { Action, ActionViewMode } from "entities/Action";
import { APIRequest } from "constants/AppsmithActionConstants/ActionConstants";
import { WidgetType } from "constants/WidgetConstants";
export interface CreateActionRequest<T> extends APIRequest {
datasourceId: string;
@ -75,6 +76,11 @@ export interface ActionExecutionResponse {
};
}
export interface SuggestedWidget {
type: WidgetType;
bindingQuery: string;
}
export interface ActionResponse {
body: unknown;
headers: Record<string, string[]>;
@ -83,6 +89,7 @@ export interface ActionResponse {
duration: string;
size: string;
isExecutionSuccess?: boolean;
suggestedWidgets?: SuggestedWidget[];
messages?: Array<string>;
}

View File

@ -0,0 +1,3 @@
<svg width="9" height="41" viewBox="0 0 9 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.14645 40.3536C4.34171 40.5488 4.65829 40.5488 4.85355 40.3536L8.03553 37.1716C8.2308 36.9763 8.2308 36.6597 8.03553 36.4645C7.84027 36.2692 7.52369 36.2692 7.32843 36.4645L4.5 39.2929L1.67157 36.4645C1.47631 36.2692 1.15973 36.2692 0.964466 36.4645C0.769204 36.6597 0.769204 36.9763 0.964466 37.1716L4.14645 40.3536ZM5.5 0C5.5 0.557191 5.05719 1 4.5 1V2C5.60948 2 6.5 1.10948 6.5 0H5.5ZM4.5 1C3.94281 1 3.5 0.557191 3.5 0H2.5C2.5 1.10948 3.39052 2 4.5 2V1ZM4 1.5V20H5V1.5H4ZM4 20V40H5V20H4Z" fill="#E0DEDE"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1,6 @@
<svg width="41" height="10" xmlns="http://www.w3.org/2000/svg" fill="none">
<g>
<path transform="rotate(-90 20.6408 5.10183)" id="svg_1" fill="#E0DEDE" d="m20.28725,25.20543c0.19526,0.1952 0.51184,0.1952 0.7071,0l3.18198,-3.182c0.19527,-0.1953 0.19527,-0.5119 0,-0.7071c-0.19526,-0.1953 -0.51184,-0.1953 -0.7071,0l-2.82843,2.8284l-2.82843,-2.8284c-0.19526,-0.1953 -0.51184,-0.1953 -0.7071,0c-0.19527,0.1952 -0.19527,0.5118 0,0.7071l3.18198,3.182zm1.35355,-40.3536c0,0.55719 -0.44281,1 -1,1l0,1c1.10948,0 2,-0.89052 2,-2l-1,0zm-1,1c-0.55719,0 -1,-0.44281 -1,-1l-1,0c0,1.10948 0.89052,2 2,2l0,-1zm-0.5,0.5l0,18.5l1,0l0,-18.5l-1,0zm0,18.5l0,20l1,0l0,-20l-1,0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View File

@ -4,8 +4,9 @@ import { WIDGET_PADDING } from "constants/WidgetConstants";
import { generateClassName } from "utils/generators";
import styled from "styled-components";
import { useClickOpenPropPane } from "utils/hooks/useClickOpenPropPane";
import { stopEventPropagation } from "utils/AppsmithUtils";
import { Layers } from "constants/Layers";
import { useSelector } from "react-redux";
import { snipingModeSelector } from "../../../selectors/editorSelectors";
const PositionedWidget = styled.div`
&:hover {
@ -27,6 +28,7 @@ export function PositionedContainer(props: PositionedContainerProps) {
const y = props.style.yPosition + (props.style.yPositionUnit || "px");
const padding = WIDGET_PADDING;
const openPropertyPane = useClickOpenPropPane();
const isSnipingMode = useSelector(snipingModeSelector);
// memoized classname
const containerClassName = useMemo(() => {
return (
@ -54,10 +56,17 @@ export function PositionedContainer(props: PositionedContainerProps) {
};
}, [props.style]);
const openPropPane = useCallback((e) => openPropertyPane(e, props.widgetId), [
props.widgetId,
openPropertyPane,
]);
const openPropPane = useCallback(
(e) => {
openPropertyPane(e, props.widgetId);
},
[props.widgetId, openPropertyPane],
);
// TODO: Experimental fix for sniping mode. This should be handled with a single event
const stopEventPropagation = (e: any) => {
!isSnipingMode && e.stopPropagation();
};
return (
<PositionedWidget

View File

@ -0,0 +1,184 @@
import React from "react";
import { Collapsible } from ".";
import Icon, { IconSize } from "components/ads/Icon";
import styled from "styled-components";
import LongArrowSVG from "assets/images/long-arrow-bottom.svg";
import Tooltip from "components/ads/Tooltip";
import { useEntityLink } from "../Debugger/hooks";
import Text, { TextType } from "components/ads/Text";
import { Classes } from "components/ads/common";
import { getTypographyByKey } from "constants/DefaultTheme";
import { useSelector } from "store";
import { getDataTree } from "selectors/dataTreeSelectors";
import { isAction, isWidget } from "workers/evaluationUtils";
import { useCallback } from "react";
import AnalyticsUtil from "utils/AnalyticsUtil";
import {
createMessage,
INCOMING_ENTITIES,
NO_INCOMING_ENTITIES,
NO_OUTGOING_ENTITIES,
OUTGOING_ENTITIES,
SEE_CONNECTED_ENTITIES,
} from "constants/messages";
const ConnectionType = styled.span`
span:nth-child(2) {
padding-left: ${(props) => props.theme.spaces[2] - 1}px;
}
padding-bottom: ${(props) => props.theme.spaces[2]}px;
`;
const ConnectionWrapper = styled.div`
margin: ${(props) => props.theme.spaces[1]}px
${(props) => props.theme.spaces[0] + 2}px;
`;
const NoConnections = styled.div`
width: 100%;
background-color: ${(props) =>
props.theme.colors.actionSidePane.noConnections};
padding: ${(props) => props.theme.spaces[4] + 1}px
${(props) => props.theme.spaces[3]}px;
.${Classes.TEXT} {
color: ${(props) => props.theme.colors.actionSidePane.noConnectionsText};
}
`;
const ConnectionFlow = styled.div`
display: flex;
align-items: center;
flex-direction: column;
img {
padding-top: ${(props) => props.theme.spaces[1]}px;
padding-bottom: ${(props) => props.theme.spaces[2] + 1}px;
}
`;
const ConnectionsContainer = styled.span`
width: 100%;
background-color: ${(props) =>
props.theme.colors.actionSidePane.noConnections};
display: flex;
flex-wrap: wrap;
padding: ${(props) => props.theme.spaces[2] + 1}px;
.connection {
border: 1px solid
${(props) => props.theme.colors.actionSidePane.connectionBorder};
padding: ${(props) => props.theme.spaces[0] + 2}px
${(props) => props.theme.spaces[1]}px;
${(props) => getTypographyByKey(props, "p3")}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
:hover {
border: 1px solid
${(props) => props.theme.colors.actionSidePane.connectionHover};
color: ${(props) => props.theme.colors.actionSidePane.connectionHover};
}
}
`;
const useGetEntityType = () => {
const dataTree = useSelector(getDataTree);
const getEntityType = useCallback((name) => {
if (isWidget(dataTree[name])) {
return "widget";
} else if (isAction(dataTree[name])) {
return "integration";
}
}, []);
return getEntityType;
};
function Dependencies(props: any) {
const { navigateToEntity } = useEntityLink();
const getEntityType = useGetEntityType();
const onClick = (entityName: string) => {
navigateToEntity(entityName);
AnalyticsUtil.logEvent("ASSOCIATED_ENTITY_CLICK", {
screen: "INTEGRATION",
});
};
return props.dependencies.length ? (
<ConnectionsContainer>
{props.dependencies.map((entityName: string) => {
const entityType = getEntityType(entityName);
return (
<Tooltip
content={`Open ${entityType}`}
disabled={!entityType}
hoverOpenDelay={1000}
key={entityName}
>
<ConnectionWrapper>
<span className="connection" onClick={() => onClick(entityName)}>
{entityName}
</span>
</ConnectionWrapper>
</Tooltip>
);
})}
</ConnectionsContainer>
) : (
<NoConnections>
<Text type={TextType.P1}>{props.placeholder}</Text>
</NoConnections>
);
}
type ConnectionsProps = {
actionName: string;
entityDependencies: {
inverseDependencies: string[];
directDependencies: string[];
} | null;
};
function Connections(props: ConnectionsProps) {
return (
<Collapsible label="Relationships">
<span className="description">
{createMessage(SEE_CONNECTED_ENTITIES)}
</span>
<ConnectionType className="icon-text">
<Icon keepColors name="trending-flat" size={IconSize.MEDIUM} />
<span className="connection-type">
{createMessage(INCOMING_ENTITIES)}
</span>
</ConnectionType>
{/* Direct Dependencies */}
<Dependencies
dependencies={props.entityDependencies?.directDependencies ?? []}
placeholder={createMessage(NO_INCOMING_ENTITIES)}
/>
<ConnectionFlow>
<img src={LongArrowSVG} />
{props.actionName}
<img src={LongArrowSVG} />
</ConnectionFlow>
<ConnectionType className="icon-text">
<span className="connection-type">
{createMessage(OUTGOING_ENTITIES)}
</span>
<Icon keepColors name="trending-flat" size={IconSize.MEDIUM} />
</ConnectionType>
{/* Inverse dependencies */}
<Dependencies
dependencies={props.entityDependencies?.inverseDependencies ?? []}
placeholder={createMessage(NO_OUTGOING_ENTITIES)}
/>
</Collapsible>
);
}
export default Connections;

View File

@ -0,0 +1,211 @@
import { getTypographyByKey } from "constants/DefaultTheme";
import { WidgetTypes } from "constants/WidgetConstants";
import React from "react";
import { useDispatch } from "react-redux";
import styled from "styled-components";
import { generateReactKey } from "utils/generators";
import { Collapsible } from ".";
import Tooltip from "components/ads/Tooltip";
import { addSuggestedWidget } from "actions/widgetActions";
import AnalyticsUtil from "utils/AnalyticsUtil";
import {
ADD_NEW_WIDGET,
createMessage,
SUGGESTED_WIDGETS,
SUGGESTED_WIDGET_DESCRIPTION,
SUGGESTED_WIDGET_TOOLTIP,
} from "constants/messages";
import { SuggestedWidget } from "api/ActionAPI";
const WidgetList = styled.div`
${(props) => getTypographyByKey(props, "p1")}
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
img {
max-width: 100%;
}
.image-wrapper {
position: relative;
margin-top: ${(props) => props.theme.spaces[1]}px;
}
.widget:hover {
cursor: pointer;
}
.widget:not(:first-child) {
margin-top: 24px;
}
`;
const WidgetOverlay = styled.div`
position: absolute;
top: 0;
left: 0;
width: 100%;
height: calc(100% - ${(props) => props.theme.spaces[1]}px);
&:hover {
display: block;
background: rgba(0, 0, 0, 0.6);
}
`;
type WidgetBindingInfo = {
label: string;
propertyName: string;
widgetName: string;
image?: string;
};
export const WIDGET_DATA_FIELD_MAP: Record<string, WidgetBindingInfo> = {
[WidgetTypes.LIST_WIDGET]: {
label: "items",
propertyName: "listData",
widgetName: "List",
image:
"https://s3.us-east-2.amazonaws.com/assets.appsmith.com/widgetSuggestion/list.svg",
},
[WidgetTypes.TABLE_WIDGET]: {
label: "tabledata",
propertyName: "tableData",
widgetName: "Table",
image:
"https://s3.us-east-2.amazonaws.com/assets.appsmith.com/widgetSuggestion/table.svg",
},
[WidgetTypes.CHART_WIDGET]: {
label: "chart-series-data-control",
propertyName: "chartData",
widgetName: "Chart",
image:
"https://s3.us-east-2.amazonaws.com/assets.appsmith.com/widgetSuggestion/chart.svg",
},
[WidgetTypes.DROP_DOWN_WIDGET]: {
label: "options",
propertyName: "options",
widgetName: "Select",
image:
"https://s3.us-east-2.amazonaws.com/assets.appsmith.com/widgetSuggestion/dropdown.svg",
},
[WidgetTypes.TEXT_WIDGET]: {
label: "text",
propertyName: "text",
widgetName: "Text",
image:
"https://s3.us-east-2.amazonaws.com/assets.appsmith.com/widgetSuggestion/text.svg",
},
[WidgetTypes.INPUT_WIDGET]: {
label: "text",
propertyName: "defaultText",
widgetName: "Input",
image:
"https://s3.us-east-2.amazonaws.com/assets.appsmith.com/widgetSuggestion/input.svg",
},
};
function getWidgetProps(
suggestedWidget: SuggestedWidget,
widgetInfo: WidgetBindingInfo,
actionName: string,
) {
const fieldName = widgetInfo.propertyName;
switch (suggestedWidget.type) {
case WidgetTypes.TABLE_WIDGET:
return {
type: WidgetTypes.TABLE_WIDGET,
props: {
[fieldName]: `{{${actionName}.${suggestedWidget.bindingQuery}}}`,
dynamicBindingPathList: [{ key: "tableData" }],
},
parentRowSpace: 10,
};
case WidgetTypes.CHART_WIDGET:
const reactKey = generateReactKey();
return {
type: suggestedWidget.type,
props: {
[fieldName]: {
[reactKey]: {
seriesName: "Sales",
data: `{{${actionName}.${suggestedWidget.bindingQuery}}}`,
},
},
dynamicBindingPathList: [{ key: `chartData.${reactKey}.data` }],
},
};
default:
return {
type: suggestedWidget.type,
props: {
[fieldName]: `{{${actionName}.${suggestedWidget.bindingQuery}}}`,
dynamicBindingPathList: [{ key: widgetInfo.propertyName }],
},
};
}
}
type SuggestedWidgetProps = {
actionName: string;
suggestedWidgets: SuggestedWidget[];
hasWidgets: boolean;
};
function SuggestedWidgets(props: SuggestedWidgetProps) {
const dispatch = useDispatch();
const addWidget = (
suggestedWidget: SuggestedWidget,
widgetInfo: WidgetBindingInfo,
) => {
const payload = getWidgetProps(
suggestedWidget,
widgetInfo,
props.actionName,
);
AnalyticsUtil.logEvent("SUGGESTED_WIDGET_CLICK", {
widget: suggestedWidget.type,
});
dispatch(addSuggestedWidget(payload));
};
const label = props.hasWidgets
? createMessage(ADD_NEW_WIDGET)
: createMessage(SUGGESTED_WIDGETS);
return (
<Collapsible label={label}>
<div className="description">
{createMessage(SUGGESTED_WIDGET_DESCRIPTION)}{" "}
</div>
<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">
{widgetInfo.image && <img src={widgetInfo.image} />}
<WidgetOverlay />
</div>
</Tooltip>
</div>
);
})}
</WidgetList>
</Collapsible>
);
}
export default SuggestedWidgets;

View File

@ -0,0 +1,249 @@
import React, { useMemo } from "react";
import styled from "styled-components";
import { Collapse, Classes as BPClasses } from "@blueprintjs/core";
import Icon, { IconSize } from "components/ads/Icon";
import { Classes, Variant } from "components/ads/common";
import Text, { TextType } from "components/ads/Text";
import { useState } from "react";
import history from "utils/history";
import { getTypographyByKey } from "constants/DefaultTheme";
import Connections from "./Connections";
import SuggestedWidgets from "./SuggestedWidgets";
import { ReactNode } from "react";
import { useEffect } from "react";
import Button, { Category, Size } from "components/ads/Button";
import { bindDataOnCanvas } from "../../../actions/actionActions";
import { useParams } from "react-router";
import { ExplorerURLParams } from "pages/Editor/Explorer/helpers";
import { useDispatch, useSelector } from "react-redux";
import { getWidgets } from "sagas/selectors";
import AnalyticsUtil from "../../../utils/AnalyticsUtil";
import { AppState } from "reducers";
import { getDependenciesFromInverseDependencies } from "../Debugger/helpers";
import { BUILDER_PAGE_URL } from "constants/routes";
import {
BACK_TO_CANVAS,
createMessage,
NO_CONNECTIONS,
} from "constants/messages";
import {
SuggestedWidget,
SuggestedWidget as SuggestedWidgetsType,
} from "api/ActionAPI";
const SideBar = styled.div`
padding: ${(props) => props.theme.spaces[0]}px
${(props) => props.theme.spaces[3]}px ${(props) => props.theme.spaces[4]}px;
overflow: auto;
height: 100%;
width: 100%;
& > div {
margin-top: ${(props) => props.theme.spaces[11]}px;
}
.icon-text {
display: flex;
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
.connection-type {
${(props) => getTypographyByKey(props, "p1")}
}
}
.icon-text:nth-child(2) {
padding-top: ${(props) => props.theme.spaces[7]}px;
}
.description {
${(props) => getTypographyByKey(props, "p1")}
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
padding-bottom: ${(props) => props.theme.spaces[7]}px;
}
`;
const Label = styled.span`
cursor: pointer;
`;
const CollapsibleWrapper = styled.div<{ isOpen: boolean }>`
.${BPClasses.COLLAPSE_BODY} {
padding-top: ${(props) => props.theme.spaces[3]}px;
}
& > .icon-text:first-child {
color: ${(props) => props.theme.colors.actionSidePane.collapsibleIcon};
${(props) => getTypographyByKey(props, "h4")}
cursor: pointer;
.${Classes.ICON} {
${(props) => !props.isOpen && `transform: rotate(-90deg);`}
}
.label {
padding-left: ${(props) => props.theme.spaces[1] + 1}px;
}
}
`;
const SnipingWrapper = styled.div`
${(props) => getTypographyByKey(props, "p1")}
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
img {
max-width: 100%;
}
.image-wrapper {
position: relative;
margin-top: ${(props) => props.theme.spaces[1]}px;
}
.widget:hover {
cursor: pointer;
}
`;
const Placeholder = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex: 1;
height: 100%;
padding: ${(props) => props.theme.spaces[8]}px;
text-align: center;
`;
const BackButton = styled.div`
display: flex;
cursor: pointer;
margin-left: ${(props) => props.theme.spaces[1] + 1}px;
.${Classes.TEXT} {
margin-left: ${(props) => props.theme.spaces[4] + 1}px;
}
`;
type CollapsibleProps = {
expand?: boolean;
children: ReactNode;
label: string;
};
export function Collapsible({
children,
expand = true,
label,
}: CollapsibleProps) {
const [isOpen, setIsOpen] = useState(!!expand);
useEffect(() => {
setIsOpen(expand);
}, [expand]);
return (
<CollapsibleWrapper isOpen={isOpen}>
<Label className="icon-text" onClick={() => setIsOpen(!isOpen)}>
<Icon name="downArrow" size={IconSize.XXS} />
<span className="label">{label}</span>
</Label>
<Collapse isOpen={isOpen} keepChildrenMounted>
{children}
</Collapse>
</CollapsibleWrapper>
);
}
function ActionSidebar({
actionName,
hasResponse,
suggestedWidgets,
}: {
actionName: string;
hasResponse: boolean;
suggestedWidgets?: SuggestedWidgetsType[];
}) {
const dispatch = useDispatch();
const widgets = useSelector(getWidgets);
const { applicationId, pageId } = useParams<ExplorerURLParams>();
const params = useParams<{ apiId?: string; queryId?: string }>();
const handleBindData = () => {
AnalyticsUtil.logEvent("SELECT_IN_CANVAS_CLICK", {
actionName: actionName,
apiId: params.apiId || params.queryId,
appId: applicationId,
});
dispatch(
bindDataOnCanvas({
queryId: (params.apiId || params.queryId) as string,
applicationId,
pageId,
}),
);
};
const hasWidgets = Object.keys(widgets).length > 1;
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
const entityDependencies = useMemo(
() =>
getDependenciesFromInverseDependencies(
deps.inverseDependencyMap,
actionName,
),
[actionName, deps.inverseDependencyMap],
);
const hasConnections =
entityDependencies &&
(entityDependencies?.directDependencies.length > 0 ||
entityDependencies?.inverseDependencies.length > 0);
const showSuggestedWidgets =
hasResponse && suggestedWidgets && !!suggestedWidgets.length;
const showSnipingMode = hasResponse && hasWidgets;
if (!hasConnections && !showSuggestedWidgets && !showSnipingMode) {
return <Placeholder>{createMessage(NO_CONNECTIONS)}</Placeholder>;
}
const navigeteToCanvas = () => {
history.push(BUILDER_PAGE_URL(applicationId, pageId));
};
return (
<SideBar>
<BackButton onClick={navigeteToCanvas}>
<Icon keepColors name="chevron-left" size={IconSize.XXS} />
<Text type={TextType.H6}>{createMessage(BACK_TO_CANVAS)}</Text>
</BackButton>
{hasConnections && (
<Connections
actionName={actionName}
entityDependencies={entityDependencies}
/>
)}
{showSuggestedWidgets && (
<SuggestedWidgets
actionName={actionName}
hasWidgets={hasWidgets}
suggestedWidgets={suggestedWidgets as SuggestedWidget[]}
/>
)}
{hasResponse && Object.keys(widgets).length > 1 && (
<Collapsible label="Connect Widget">
<div className="description">Go to canvas and select widgets</div>
<SnipingWrapper>
<Button
category={Category.tertiary}
className={"t--select-in-canvas"}
onClick={handleBindData}
size={Size.medium}
tag="button"
text="Select Widget"
type="button"
variant={Variant.info}
/>
</SnipingWrapper>
</Collapsible>
)}
</SideBar>
);
}
export default ActionSidebar;

View File

@ -1,55 +1,112 @@
/* eslint-disable prefer-const */
import { Collapse } from "@blueprintjs/core";
import React, { memo, ReactNode, useMemo, useState } from "react";
import React, { useCallback, useMemo } from "react";
import { useSelector } from "react-redux";
import { AppState } from "reducers";
import styled from "styled-components";
import Icon, { IconSize } from "components/ads/Icon";
import { Classes } from "components/ads/common";
import InspectElement from "assets/images/InspectElement.svg";
import { SourceEntity } from "entities/AppsmithConsole";
import { createMessage, INSPECT_ENTITY_BLANK_STATE } from "constants/messages";
import { ReactComponent as LongArrowSVG } from "assets/images/long-arrow-right.svg";
import {
createMessage,
INCOMING_ENTITIES,
INSPECT_ENTITY_BLANK_STATE,
NO_INCOMING_ENTITIES,
NO_OUTGOING_ENTITIES,
OUTGOING_ENTITIES,
} from "constants/messages";
import { getDependenciesFromInverseDependencies } from "./helpers";
import { useEntityLink, useSelectedEntity } from "./hooks";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { getTypographyByKey } from "constants/DefaultTheme";
import { getDataTree } from "selectors/dataTreeSelectors";
import { isAction, isWidget } from "workers/evaluationUtils";
import Tooltip from "components/ads/Tooltip";
import Text, { TextType } from "components/ads/Text";
const CollapsibleWrapper = styled.div<{ step: number; isOpen: boolean }>`
margin-left: ${(props) => props.step * 10}px;
padding-top: ${(props) => props.theme.spaces[3]}px;
const ConnectionType = styled.span`
span:nth-child(2) {
padding-left: ${(props) => props.theme.spaces[2] - 1}px;
}
padding-bottom: ${(props) => props.theme.spaces[2]}px;
`;
.label-wrapper {
const ConnectionWrapper = styled.div`
margin: ${(props) => props.theme.spaces[1]}px
${(props) => props.theme.spaces[0] + 2}px;
`;
const ConnectionsContainer = styled.span`
background-color: ${(props) =>
props.theme.colors.actionSidePane.noConnections};
display: flex;
flex-wrap: wrap;
padding: ${(props) => props.theme.spaces[2] + 1.5}px
${(props) => props.theme.spaces[2] + 1}px;
.connection {
border: 1px solid
${(props) => props.theme.colors.actionSidePane.connectionBorder};
padding: ${(props) => props.theme.spaces[0] + 2}px
${(props) => props.theme.spaces[1]}px;
${(props) => getTypographyByKey(props, "p3")}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
:hover {
border: 1px solid
${(props) => props.theme.colors.actionSidePane.connectionHover};
color: ${(props) => props.theme.colors.actionSidePane.connectionHover};
}
}
`;
const NoConnections = styled.div`
background-color: ${(props) =>
props.theme.colors.actionSidePane.noConnections};
padding: ${(props) => props.theme.spaces[4] + 1}px
${(props) => props.theme.spaces[3]}px;
.${Classes.TEXT} {
color: ${(props) => props.theme.colors.actionSidePane.noConnectionsText};
}
`;
const ConnectionFlow = styled.div`
display: flex;
align-items: center;
margin-top: 24px;
svg {
margin: 0px 4px;
}
span {
margin: 0px 4px;
}
`;
const Wrapper = styled.div`
display: flex;
height: 100%;
align-items: center;
justify-content: center;
padding: 0px 100px;
.icon-text {
display: flex;
flex-direction: row;
font-weight: ${(props) => props.theme.fontWeights[2]};
margin-left: ${(props) => props.theme.spaces[2] + 1}px;
span {
margin-left: ${(props) => props.theme.spaces[3] - 1}px;
.connection-type {
${(props) => getTypographyByKey(props, "p1")}
}
}
.${Classes.ICON} {
${(props) => !props.isOpen && `transform: rotate(-90deg);`}
.icon-text:nth-child(2) {
padding-top: ${(props) => props.theme.spaces[7]}px;
}
`;
const DependenciesWrapper = styled.div`
padding: ${(props) => props.theme.spaces[7]}px
${(props) => props.theme.spaces[13] + 1}px;
color: ${(props) => props.theme.colors.debugger.inspectElement.color};
.no-dependencies {
margin-left: ${(props) => props.theme.spaces[4]}px;
}
`;
const StyledSpan = styled.div<{ step: number }>`
padding-top: ${(props) => props.theme.spaces[3]}px;
padding-left: ${(props) => props.theme.spaces[6] + 1}px;
margin-left: ${(props) => props.theme.spaces[4]}px;
border-left: solid 1px rgba(147, 144, 144, 0.7);
text-decoration-line: underline;
cursor: pointer;
`;
const BlankStateContainer = styled.div`
height: 100%;
display: flex;
@ -64,41 +121,31 @@ const BlankStateContainer = styled.div`
}
`;
function EntityDeps() {
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
const selectedEntity = useSelectedEntity();
const ConnectionContainer = styled.div`
width: 100%;
`;
const entityDependencies: {
directDependencies: string[];
type ConnectionsProps = {
entityName: string;
entityDependencies: {
inverseDependencies: string[];
} | null = useMemo(
() =>
getDependenciesFromInverseDependencies(
deps.inverseDependencyMap,
selectedEntity ? selectedEntity.name : null,
),
[selectedEntity, deps.inverseDependencyMap],
);
directDependencies: string[];
} | null;
};
if (!selectedEntity || !entityDependencies) return <BlankState />;
const useGetEntityType = () => {
const dataTree = useSelector(getDataTree);
return (
<div>
<MemoizedDependencyHierarchy
dependencies={entityDependencies.directDependencies}
entityName={`Dependencies of ${selectedEntity.name}`}
selectedEntity={selectedEntity}
type="dependencies"
/>
<MemoizedDependencyHierarchy
dependencies={entityDependencies.inverseDependencies}
entityName={`References of ${selectedEntity.name}`}
selectedEntity={selectedEntity}
type="references"
/>
</div>
);
}
const getEntityType = useCallback((name) => {
if (isWidget(dataTree[name])) {
return "widget";
} else if (isAction(dataTree[name])) {
return "integration";
}
}, []);
return getEntityType;
};
function BlankState() {
return (
@ -109,68 +156,102 @@ function BlankState() {
);
}
function DependencyHierarchy(props: {
dependencies: string[];
entityName: string;
selectedEntity: SourceEntity;
type: string;
}) {
function Dependencies(props: any) {
const { navigateToEntity } = useEntityLink();
const label = props.dependencies.length
? props.entityName
: `No ${props.type} exist for ${props.selectedEntity.name}`;
const getEntityType = useGetEntityType();
return (
<DependenciesWrapper>
{props.dependencies.length ? (
<Collapsible label={label} step={0}>
{props.dependencies.map((item) => {
return (
<StyledSpan
className={`t--${props.type}-item`}
key={`${props.selectedEntity.id}-${item}`}
onClick={(e) => {
e.stopPropagation();
navigateToEntity(item);
}}
step={2}
>
{item}
</StyledSpan>
);
})}
</Collapsible>
) : (
<span className="no-dependencies">{label}</span>
)}
</DependenciesWrapper>
);
}
const MemoizedDependencyHierarchy = memo(DependencyHierarchy);
const onClick = (entityName: string) => {
navigateToEntity(entityName);
AnalyticsUtil.logEvent("ASSOCIATED_ENTITY_CLICK", {
screen: "INTEGRATION",
});
};
function Collapsible(props: {
label: string;
step: number;
children: ReactNode;
}) {
const [isOpen, setIsOpen] = useState(true);
return props.dependencies.length ? (
<ConnectionsContainer>
{props.dependencies.map((entityName: string) => {
const entityType = getEntityType(entityName);
return (
<CollapsibleWrapper
isOpen={isOpen}
onClick={(e) => {
e.stopPropagation();
setIsOpen(!isOpen);
}}
step={props.step}
>
<div className="label-wrapper">
<Icon name={"downArrow"} size={IconSize.XXS} />
<span>{props.label}</span>
</div>
<Collapse isOpen={isOpen}>{props.children}</Collapse>
</CollapsibleWrapper>
return (
<Tooltip
content={`Open ${entityType}`}
disabled={!entityType}
hoverOpenDelay={1000}
key={entityName}
>
<ConnectionWrapper className="t--dependencies-item">
<span className="connection" onClick={() => onClick(entityName)}>
{entityName}
</span>
</ConnectionWrapper>
</Tooltip>
);
})}
</ConnectionsContainer>
) : (
<NoConnections>
<Text type={TextType.P1}>{props.placeholder}</Text>
</NoConnections>
);
}
export default EntityDeps;
function EntityDeps(props: ConnectionsProps) {
return (
<Wrapper>
<ConnectionContainer>
<ConnectionType className="icon-text">
<Icon keepColors name="trending-flat" size={IconSize.MEDIUM} />
<span className="connection-type">
{createMessage(INCOMING_ENTITIES)}
</span>
</ConnectionType>
<Dependencies
dependencies={props.entityDependencies?.directDependencies ?? []}
placeholder={createMessage(NO_INCOMING_ENTITIES)}
/>
</ConnectionContainer>
<ConnectionFlow>
<LongArrowSVG />
<span>{props.entityName}</span>
<LongArrowSVG />
</ConnectionFlow>
<ConnectionContainer>
<ConnectionType className="icon-text">
<span className="connection-type">
{createMessage(OUTGOING_ENTITIES)}
</span>
<Icon keepColors name="trending-flat" size={IconSize.MEDIUM} />
</ConnectionType>
{/* Inverse dependencies */}
<Dependencies
dependencies={props.entityDependencies?.inverseDependencies ?? []}
placeholder={createMessage(NO_OUTGOING_ENTITIES)}
/>
</ConnectionContainer>
</Wrapper>
);
}
function InspectEntity() {
const deps = useSelector((state: AppState) => state.evaluations.dependencies);
const selectedEntity = useSelectedEntity();
const entityDependencies = useMemo(
() =>
getDependenciesFromInverseDependencies(
deps.inverseDependencyMap,
selectedEntity?.name ?? "",
),
[selectedEntity?.name, deps.inverseDependencyMap],
);
if (!selectedEntity || !entityDependencies) return <BlankState />;
return (
<EntityDeps
entityDependencies={entityDependencies}
entityName={selectedEntity?.name ?? ""}
/>
);
}
export default InspectEntity;

View File

@ -13,6 +13,7 @@ import {
} from "utils/hooks/dragResizeHooks";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { commentModeSelector } from "selectors/commentsSelectors";
import { snipingModeSelector } from "selectors/editorSelectors";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
const DraggableWrapper = styled.div`
@ -53,9 +54,14 @@ export const canDrag = (
isDraggingDisabled: boolean,
props: any,
isCommentMode: boolean,
isSnipingMode: boolean,
) => {
return (
!isResizing && !isDraggingDisabled && !props?.dragDisabled && !isCommentMode
!isResizing &&
!isDraggingDisabled &&
!props?.dragDisabled &&
!isCommentMode &&
!isSnipingMode
);
};
@ -68,6 +74,7 @@ function DraggableComponent(props: DraggableComponentProps) {
const { focusWidget, selectWidget } = useWidgetSelection();
const isCommentMode = useSelector(commentModeSelector);
const isSnipingMode = useSelector(snipingModeSelector);
// Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging
// The value is boolean
@ -147,7 +154,13 @@ function DraggableComponent(props: DraggableComponentProps) {
},
canDrag: () => {
// Dont' allow drag if we're resizing or the drag of `DraggableComponent` is disabled
return canDrag(isResizing, isDraggingDisabled, props, isCommentMode);
return canDrag(
isResizing,
isDraggingDisabled,
props,
isCommentMode,
isSnipingMode,
);
},
});

View File

@ -39,6 +39,7 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import { scrollElementIntoParentCanvasView } from "utils/helpers";
import { getNearestParentCanvas } from "utils/generators";
import { commentModeSelector } from "selectors/commentsSelectors";
import { snipingModeSelector } from "selectors/editorSelectors";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
export type ResizableComponentProps = WidgetProps & {
@ -59,6 +60,7 @@ export const ResizableComponent = memo(function ResizableComponent(
} = useContext(DropTargetContext);
const isCommentMode = useSelector(commentModeSelector);
const isSnipingMode = useSelector(snipingModeSelector);
const showPropertyPane = useShowPropertyPane();
const showTableFilterPane = useShowTableFilterPane();
@ -276,7 +278,11 @@ export const ResizableComponent = memo(function ResizableComponent(
}, [props]);
const isEnabled =
!isDragging && isWidgetFocused && !props.resizeDisabled && !isCommentMode;
!isDragging &&
isWidgetFocused &&
!props.resizeDisabled &&
!isCommentMode &&
!isSnipingMode;
return (
<Resizable

View File

@ -0,0 +1,83 @@
import React, { useCallback } from "react";
import styled from "styled-components";
import { WidgetProps } from "widgets/BaseWidget";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "reducers";
import { getColorWithOpacity } from "constants/DefaultTheme";
// import AnalyticsUtil from "utils/AnalyticsUtil";
import { snipingModeSelector } from "selectors/editorSelectors";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
import { Layers } from "../../constants/Layers";
import { bindDataToWidget } from "../../actions/propertyPaneActions";
const SnipeableWrapper = styled.div<{ isFocused: boolean }>`
position: absolute;
width: calc(100% + ${WIDGET_PADDING - 5}px);
height: calc(100% + ${WIDGET_PADDING - 5}px);
transition: 0.15s ease;
text-align: center;
border: 3px solid transparent;
&:hover {
border: 3px solid
${(props) =>
props.isFocused
? getColorWithOpacity(props.theme.colors.textAnchor, 0.5)
: "transparent"};
${(props) => props.isFocused && "cursor: pointer"};
z-index: ${Layers.snipeableZone + 1} !important;
}
`;
type SnipeableComponentProps = WidgetProps;
function SnipeableComponent(props: SnipeableComponentProps) {
const { focusWidget } = useWidgetSelection();
const dispatch = useDispatch();
const isSnipingMode = useSelector(snipingModeSelector);
const isFocusedWidget = useSelector(
(state: AppState) =>
state.ui.widgetDragResize.focusedWidget === props.widgetId,
);
const handleMouseOver = (e: any) => {
focusWidget && !isFocusedWidget && focusWidget(props.widgetId);
e.stopPropagation();
};
const classNameForTesting = `t--snipeable-${props.type
.split("_")
.join("")
.toLowerCase()}`;
const className = `${classNameForTesting}`;
const onSelectWidgetToBind = useCallback(
(e) => {
dispatch(
bindDataToWidget({
widgetId: props.widgetId,
}),
);
e.stopPropagation();
},
[bindDataToWidget, props.widgetId, dispatch],
);
return isSnipingMode ? (
<SnipeableWrapper
className={className}
isFocused={isFocusedWidget}
onClick={onSelectWidgetToBind}
onMouseOver={handleMouseOver}
>
{props.children}
</SnipeableWrapper>
) : (
props.children
);
}
export default SnipeableComponent;

View File

@ -4,6 +4,8 @@ import Icon, { IconSize } from "components/ads/Icon";
import { Colors } from "constants/Colors";
import styled from "styled-components";
import { Tooltip, Classes } from "@blueprintjs/core";
import { useSelector } from "react-redux";
import { snipingModeSelector } from "selectors/editorSelectors";
// I honestly can't think of a better name for this enum
export enum Activities {
HOVERING,
@ -37,6 +39,7 @@ const SettingsWrapper = styled.div`
const WidgetName = styled.span`
margin-right: ${(props) => props.theme.spaces[1] + 1}px;
margin-left: ${(props) => props.theme.spaces[3]}px;
white-space: nowrap;
`;
const StyledErrorIcon = styled(Icon)`
@ -57,13 +60,20 @@ type SettingsControlProps = {
errorCount: number;
};
const BindDataIcon = ControlIcons.BIND_DATA_CONTROL;
const SettingsIcon = ControlIcons.SETTINGS_CONTROL;
const getStyles = (
activity: Activities,
errorCount: number,
isSnipingMode: boolean,
): CSSProperties | undefined => {
if (errorCount > 0) {
if (isSnipingMode) {
return {
background: Colors.DANUBE,
color: Colors.WHITE,
};
} else if (errorCount > 0) {
return {
background: "red",
color: Colors.WHITE,
@ -90,6 +100,7 @@ const getStyles = (
};
export function SettingsControl(props: SettingsControlProps) {
const isSnipingMode = useSelector(snipingModeSelector);
const settingsIcon = (
<SettingsIcon
color={
@ -113,7 +124,11 @@ export function SettingsControl(props: SettingsControlProps) {
return (
<StyledTooltip
content="Edit widget properties"
content={
isSnipingMode
? `Bind to widget ${props.name}`
: "Edit widget properties"
}
hoverOpenDelay={500}
position="top-right"
>
@ -121,16 +136,21 @@ export function SettingsControl(props: SettingsControlProps) {
className="t--widget-propertypane-toggle"
data-testid="t--widget-propertypane-toggle"
onClick={props.toggleSettings}
style={getStyles(props.activity, props.errorCount)}
style={getStyles(props.activity, props.errorCount, isSnipingMode)}
>
{!!props.errorCount && (
{!!props.errorCount && !isSnipingMode && (
<>
{errorIcon}
<span className="t--widget-error-count">{props.errorCount}</span>
</>
)}
<WidgetName className="t--widget-name">{props.name}</WidgetName>
{settingsIcon}
{isSnipingMode && (
<BindDataIcon color={Colors.WHITE} height={16} width={12} />
)}
<WidgetName className="t--widget-name">
{isSnipingMode ? `Bind to ${props.name}` : props.name}
</WidgetName>
{!isSnipingMode && settingsIcon}
</SettingsWrapper>
</StyledTooltip>
);

View File

@ -1,6 +1,6 @@
import React from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "reducers";
import { PropertyPaneReduxState } from "reducers/uiReducers/propertyPaneReducer";
import SettingsControl, { Activities } from "./SettingsControl";
@ -15,13 +15,15 @@ import PerformanceTracker, {
} from "utils/PerformanceTracker";
import { getIsTableFilterPaneVisible } from "selectors/tableFilterSelectors";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
import { snipingModeSelector } from "selectors/editorSelectors";
import { bindDataToWidget } from "../../../actions/propertyPaneActions";
const PositionStyle = styled.div<{ topRow: number }>`
const PositionStyle = styled.div<{ topRow: number; isSnipingMode: boolean }>`
position: absolute;
top: ${(props) =>
props.topRow > 2 ? `${-1 * props.theme.spaces[10]}px` : "calc(100%)"};
height: ${(props) => props.theme.spaces[10]}px;
right: 0;
${(props) => (props.isSnipingMode ? "left: -7px" : "right: 0")};
display: flex;
padding: 0 4px;
cursor: pointer;
@ -50,6 +52,8 @@ type WidgetNameComponentProps = {
export function WidgetNameComponent(props: WidgetNameComponentProps) {
const showPropertyPane = useShowPropertyPane();
const dispatch = useDispatch();
const isSnipingMode = useSelector(snipingModeSelector);
const showTableFilterPane = useShowTableFilterPane();
// Dispatch hook handy to set a widget as focused/selected
const { selectWidget } = useWidgetSelection();
@ -76,7 +80,13 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
const isTableFilterPaneVisible = useSelector(getIsTableFilterPaneVisible);
const togglePropertyEditor = (e: any) => {
if (
if (isSnipingMode) {
dispatch(
bindDataToWidget({
widgetId: props.widgetId,
}),
);
} else if (
(!propertyPaneState.isVisible &&
props.widgetId === propertyPaneState.widgetId) ||
props.widgetId !== propertyPaneState.widgetId
@ -110,12 +120,16 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
selectedWidget === props.widgetId ||
selectedWidgets.includes(props.widgetId);
const showWidgetName =
props.showControls ||
((focusedWidget === props.widgetId || showAsSelected) &&
!isDragging &&
!isResizing) ||
!!props.errorCount;
// in sniping mode we only show the widget name tag if it's focused.
// in case of widget selection in sniping mode, if it's successful we bind the data else carry on
// with sniping mode.
const showWidgetName = isSnipingMode
? focusedWidget === props.widgetId
: props.showControls ||
((focusedWidget === props.widgetId || showAsSelected) &&
!isDragging &&
!isResizing) ||
!!props.errorCount;
let currentActivity =
props.type === WidgetTypes.MODAL_WIDGET
@ -132,7 +146,9 @@ export function WidgetNameComponent(props: WidgetNameComponentProps) {
return showWidgetName ? (
<PositionStyle
className={isSnipingMode ? "t--settings-sniping-control" : ""}
data-testid="t--settings-controls-positioned-wrapper"
isSnipingMode={isSnipingMode}
topRow={props.topRow}
>
<ControlGroup>

View File

@ -39,7 +39,7 @@ import { DATA_SOURCES_EDITOR_ID_URL } from "constants/routes";
import Icon, { IconSize } from "components/ads/Icon";
import Text, { TextType } from "components/ads/Text";
import history from "utils/history";
import { getDatasourceInfo } from "pages/Editor/APIEditor/DatasourceList";
import { getDatasourceInfo } from "pages/Editor/APIEditor/ApiRightPane";
import * as FontFamilies from "constants/Fonts";
type ReduxStateProps = {

View File

@ -25,7 +25,7 @@ import { actionPathFromName } from "components/formControls/utils";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
const Wrapper = styled.div`
width: 60%;
width: 75%;
.dynamic-text-field {
border-radius: 4px;
font-size: 14px;

View File

@ -435,6 +435,9 @@ export type Theme = {
};
};
iconSizes: IconSizeType;
actionSidePane: {
width: number;
};
};
type IconSizeType = {
@ -1084,6 +1087,13 @@ type ColorType = {
border: string;
actionActiveBg: string;
};
actionSidePane: {
noConnections: string;
noConnectionsText: string;
connectionBorder: string;
connectionHover: string;
collapsibleIcon: string;
};
};
const notifications = {
@ -1239,6 +1249,13 @@ const mentionsInput = {
mentionsInviteBtnPlusIcon: "#6A86CE",
};
const actionSidePane = {
noConnections: "#f0f0f0",
noConnectionsText: "#e0dede",
connectionBorder: "rgba(0, 0, 0, 0.5)",
connectionHover: "#6a86ce",
collapsibleIcon: Colors.CODE_GRAY,
};
const navigationMenu = {
contentActive: "#F0F0F0",
backgroundActive: "#222222",
@ -1713,6 +1730,7 @@ export const dark: ColorType = {
border: "#69b5ff",
actionActiveBg: "#e1e1e1",
},
actionSidePane,
};
export const light: ColorType = {
@ -2188,6 +2206,7 @@ export const light: ColorType = {
border: "#69b5ff",
actionActiveBg: "#e1e1e1",
},
actionSidePane,
};
export const theme: Theme = {
@ -2549,6 +2568,9 @@ export const theme: Theme = {
},
},
},
actionSidePane: {
width: 265,
},
};
export { css, createGlobalStyle, keyframes, ThemeProvider };

View File

@ -11,6 +11,7 @@ export enum Indices {
Layer7,
Layer8,
Layer9,
Layer10,
Layer21 = 21,
LayerMax = 99999,
}
@ -48,6 +49,7 @@ export const Layers = {
productUpdates: Indices.Layer7,
portals: Indices.Layer8,
header: Indices.Layer9,
snipeableZone: Indices.Layer10,
appComments: Indices.Layer7,
max: Indices.LayerMax,
};

View File

@ -9,6 +9,8 @@ export const ReduxSagaChannels: { [key: string]: string } = {
};
export const ReduxActionTypes: { [key: string]: string } = {
BIND_DATA_TO_WIDGET: "BIND_DATA_TO_WIDGET",
BIND_DATA_ON_CANVAS: "BIND_DATA_ON_CANVAS",
INCREMENT_COMMENT_THREAD_UNREAD_COUNT:
"INCREMENT_COMMENT_THREAD_UNREAD_COUNT",
DECREMENT_COMMENT_THREAD_UNREAD_COUNT:
@ -74,6 +76,8 @@ export const ReduxActionTypes: { [key: string]: string } = {
RESOLVE_COMMENT_THREAD: "RESOLVE_COMMENT_THREAD",
SET_IS_COMMENT_THREAD_VISIBLE: "SET_IS_COMMENT_THREAD_VISIBLE",
SET_COMMENT_MODE: "SET_COMMENT_MODE",
SET_SNIPING_MODE: "SET_SNIPING_MODE",
RESET_SNIPING_MODE: "RESET_SNIPING_MODE",
HANDLE_PATH_UPDATED: "HANDLE_PATH_UPDATED",
RESET_EDITOR_REQUEST: "RESET_EDITOR_REQUEST",
RESET_EDITOR_SUCCESS: "RESET_EDITOR_SUCCESS",
@ -326,7 +330,7 @@ export const ReduxActionTypes: { [key: string]: string } = {
FOCUS_WIDGET: "FOCUS_WIDGET",
SET_WIDGET_DRAGGING: "SET_WIDGET_DRAGGING",
SET_WIDGET_RESIZING: "SET_WIDGET_RESIZING",
ADD_TABLE_WIDGET_FROM_QUERY: "ADD_TABLE_WIDGET_FROM_QUERY",
ADD_SUGGESTED_WIDGET: "ADD_SUGGESTED_WIDGET",
SEARCH_APPLICATIONS: "SEARCH_APPLICATIONS",
UPDATE_PAGE_INIT: "UPDATE_PAGE_INIT",
UPDATE_PAGE_SUCCESS: "UPDATE_PAGE_SUCCESS",

View File

@ -359,3 +359,17 @@ export const IMPORT_APPLICATION_MODAL_TITLE = () => "Import Application";
export const DELETE_CONFIRMATION_MODAL_TITLE = () => `Are you sure?`;
export const DELETE_CONFIRMATION_MODAL_SUBTITLE = (name?: string | null) =>
`You want to remove ${name} from this organization`;
// Actions Right pane
export const SEE_CONNECTED_ENTITIES = () => "See all connected entities";
export const INCOMING_ENTITIES = () => "Incoming entities";
export const NO_INCOMING_ENTITIES = () => "No incoming entities";
export const OUTGOING_ENTITIES = () => "Outgoing entities";
export const NO_OUTGOING_ENTITIES = () => "No outgoing entities";
export const NO_CONNECTIONS = () => "No connections to show here";
export const BACK_TO_CANVAS = () => "Back to canvas";
export const SUGGESTED_WIDGET_DESCRIPTION = () =>
"This will add a new widget to the canvas.";
export const ADD_NEW_WIDGET = () => "Add New Widget";
export const SUGGESTED_WIDGETS = () => "Suggested widgets";
export const SUGGESTED_WIDGET_TOOLTIP = () => "Add to canvas";

View File

@ -47,6 +47,7 @@ import { ReactComponent as BulletsIcon } from "assets/icons/control/bullets.svg"
import { ReactComponent as DividerCapRightIcon } from "assets/icons/control/divider_cap_right.svg";
import { ReactComponent as DividerCapLeftIcon } from "assets/icons/control/divider_cap_left.svg";
import { ReactComponent as DividerCapAllIcon } from "assets/icons/control/divider_cap_all.svg";
import { ReactComponent as TrendingFlat } from "assets/icons/ads/trending-flat.svg";
import { ReactComponent as AlignLeftIcon } from "assets/icons/control/align_left.svg";
import { ReactComponent as AlignRightIcon } from "assets/icons/control/align_right.svg";
import PlayIcon from "assets/icons/control/play-icon.png";
@ -300,6 +301,11 @@ export const ControlIcons: {
<DividerCapAllIcon />
</IconWrapper>
),
BIND_DATA_CONTROL: (props: IconProps) => (
<IconWrapper {...props}>
<TrendingFlat />
</IconWrapper>
),
ICON_ALIGN_LEFT: (props: IconProps) => (
<IconWrapper {...props}>
<AlignLeftIcon />

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import Icon, { IconSize } from "components/ads/Icon";
import { StyledSeparator } from "pages/Applications/ProductUpdatesModal/ReleaseComponent";
@ -8,12 +8,12 @@ import { TabComponent } from "components/ads/Tabs";
import Text, { FontWeight, TextType } from "components/ads/Text";
import { TabbedViewContainer } from "./Form";
import get from "lodash/get";
import ActionRightPane from "components/editorComponents/ActionRightPane";
const EmptyDatasourceContainer = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 280px;
padding: 50px;
border-left: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
height: 100%;
@ -28,21 +28,16 @@ const DatasourceContainer = styled.div`
margin-right: 0;
}
}
width: ${(props) => props.theme.actionSidePane.width}px;
`;
const DataSourceListWrapper = styled.div`
width: 0;
display: flex;
flex-direction: column;
height: 100%;
padding: 10px;
border-left: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
overflow: auto;
transition: width 2s;
background: white;
&.show {
width: 280px;
}
`;
const DatasourceCard = styled.div`
@ -106,6 +101,11 @@ const DataSourceNameContainer = styled.div`
}
`;
const SomeWrapper = styled.div`
border-left: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
height: 100%;
`;
export const getDatasourceInfo = (datasource: any): string => {
const info = [];
const headers = get(datasource, "datasourceConfiguration.headers", []);
@ -120,8 +120,11 @@ export const getDatasourceInfo = (datasource: any): string => {
return info.join(" | ");
};
export default function DataSourceList(props: any) {
export default function ApiRightPane(props: any) {
const [selectedIndex, setSelectedIndex] = useState(0);
useEffect(() => {
if (!!props.hasResponse) setSelectedIndex(1);
}, [props.hasResponse]);
return (
<DatasourceContainer>
<TabbedViewContainer>
@ -132,8 +135,6 @@ export default function DataSourceList(props: any) {
{
key: "datasources",
title: "Datasources",
icon: "datasource",
iconSize: IconSize.LARGE,
panelComponent:
props.datasources && props.datasources.length > 0 ? (
<DataSourceListWrapper
@ -197,6 +198,19 @@ export default function DataSourceList(props: any) {
</EmptyDatasourceContainer>
),
},
{
key: "Connections",
title: "Connections",
panelComponent: (
<SomeWrapper>
<ActionRightPane
actionName={props.actionName}
hasResponse={props.hasResponse}
suggestedWidgets={props.suggestedWidgets}
/>
</SomeWrapper>
),
},
]}
/>
</TabbedViewContainer>

View File

@ -1,10 +1,10 @@
import React, { useState } from "react";
import { connect, useSelector, useDispatch } from "react-redux";
import { connect, useDispatch, useSelector } from "react-redux";
import {
change,
formValueSelector,
InjectedFormProps,
reduxForm,
change,
} from "redux-form";
import {
HTTP_METHOD_OPTIONS,
@ -13,15 +13,19 @@ import {
import styled from "styled-components";
import FormLabel from "components/editorComponents/FormLabel";
import FormRow from "components/editorComponents/FormRow";
import { PaginationField } from "api/ActionAPI";
import { PaginationField, SuggestedWidget } from "api/ActionAPI";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import Pagination from "./Pagination";
import { Action, PaginationType } from "entities/Action";
import { setGlobalSearchQuery } from "actions/globalSearchActions";
import { toggleShowGlobalSearchModal } from "actions/globalSearchActions";
import {
setGlobalSearchQuery,
toggleShowGlobalSearchModal,
} from "actions/globalSearchActions";
import KeyValueFieldArray from "components/editorComponents/form/fields/KeyValueFieldArray";
import PostBodyData from "./PostBodyData";
import ApiResponseView from "components/editorComponents/ApiResponseView";
import ApiResponseView, {
EMPTY_RESPONSE,
} from "components/editorComponents/ApiResponseView";
import EmbeddedDatasourcePathField from "components/editorComponents/form/fields/EmbeddedDatasourcePathField";
import { AppState } from "reducers";
import { getApiName } from "selectors/formSelectors";
@ -32,7 +36,7 @@ import { ExplorerURLParams } from "../Explorer/helpers";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import Icon from "components/ads/Icon";
import Button, { Size } from "components/ads/Button";
import { TabComponent, TabTitle } from "components/ads/Tabs";
import { TabComponent } from "components/ads/Tabs";
import { EditorTheme } from "components/editorComponents/CodeEditor/EditorConfig";
import Text, { Case, TextType } from "components/ads/Text";
import { Classes, Variant } from "components/ads/common";
@ -43,16 +47,21 @@ import AnalyticsUtil from "utils/AnalyticsUtil";
import CloseEditor from "components/editorComponents/CloseEditor";
import { useParams } from "react-router";
import { Icon as ButtonIcon } from "@blueprintjs/core";
import { IconSize } from "components/ads/Icon";
import get from "lodash/get";
import DataSourceList from "./DatasourceList";
import DataSourceList from "./ApiRightPane";
import { Datasource } from "entities/Datasource";
import { getActionResponses } from "../../../selectors/entitiesSelector";
import { isEmpty } from "lodash";
const Form = styled.form`
display: flex;
flex-direction: column;
height: calc(
100vh - ${(props) => props.theme.smallHeaderHeight} -
${(props) => props.theme.backBanner}
100vh -
(
${(props) => props.theme.smallHeaderHeight} +
${(props) => props.theme.backBanner}
)
);
overflow: hidden;
width: 100%;
@ -205,6 +214,8 @@ interface APIFormProps {
datasources?: any;
currentPageId?: string;
applicationId?: string;
hasResponse: boolean;
suggestedWidgets?: SuggestedWidget[];
updateDatasource: (datasource: Datasource) => void;
}
@ -310,24 +321,6 @@ function ImportedHeaderKeyValue(props: { headers: any }) {
);
}
const DatasourceListTrigger = styled.div`
position: absolute;
right: 10px;
top: 7px;
display: flex;
align-items: center;
cursor: pointer;
color: #939090;
&:hover {
span {
color: #090707;
}
svg path {
fill: #090707;
}
}
`;
const BoundaryContainer = styled.div`
border: 1px solid transparent;
border-right: none;
@ -361,17 +354,6 @@ function renderImportedHeadersButton(
);
}
const CloseIconContainer = styled.div`
position: absolute;
top: 12px;
right: 10px;
svg {
path {
fill: #a9a7a7;
}
}
`;
function renderHelpSection(
handleClickLearnHow: any,
setApiBindHelpSectionVisible: any,
@ -434,9 +416,6 @@ function ImportedHeaders(props: { headers: any }) {
function ApiEditorForm(props: Props) {
const [selectedIndex, setSelectedIndex] = useState(0);
const [showDatasources, toggleDatasources] = useState(
!!props.datasources.length,
);
const [
apiBindHelpSectionVisible,
setApiBindHelpSectionVisible,
@ -477,6 +456,7 @@ function ApiEditorForm(props: Props) {
dispatch(toggleShowGlobalSearchModal());
AnalyticsUtil.logEvent("OPEN_OMNIBAR", { source: "LEARN_HOW_DATASOURCE" });
};
return (
<>
<CloseEditor />
@ -625,14 +605,6 @@ function ApiEditorForm(props: Props) {
},
]}
/>
{!showDatasources && (
<DatasourceListTrigger
onClick={() => toggleDatasources(!showDatasources)}
>
<Icon name="datasource" size={IconSize.LARGE} />
<TabTitle>Datasources</TabTitle>
</DatasourceListTrigger>
)}
</TabbedViewContainer>
<ApiResponseView
apiName={actionName}
@ -640,24 +612,15 @@ function ApiEditorForm(props: Props) {
theme={theme}
/>
</SecondaryWrapper>
{showDatasources && (
<>
<DataSourceList
applicationId={props.applicationId}
currentPageId={props.currentPageId}
datasources={props.datasources}
onClick={updateDatasource}
/>
<CloseIconContainer>
<Icon
className="close-modal-icon"
name="close-modal"
onClick={() => toggleDatasources(!showDatasources)}
size={IconSize.SMALL}
/>
</CloseIconContainer>
</>
)}
<DataSourceList
actionName={actionName}
applicationId={props.applicationId}
currentPageId={props.currentPageId}
datasources={props.datasources}
hasResponse={props.hasResponse}
onClick={updateDatasource}
suggestedWidgets={props.suggestedWidgets}
/>
</Wrapper>
</Form>
</>
@ -716,6 +679,16 @@ export default connect((state: AppState, props: { pluginId: string }) => {
}
const hintMessages = selector(state, "datasource.messages");
const responses = getActionResponses(state);
let hasResponse = false;
let suggestedWidgets;
if (apiId && apiId in responses) {
const response = responses[apiId] || EMPTY_RESPONSE;
hasResponse =
!isEmpty(response.statusCode) && response.statusCode[0] === "2";
suggestedWidgets = response.suggestedWidgets;
}
return {
actionName,
apiId,
@ -730,6 +703,8 @@ export default connect((state: AppState, props: { pluginId: string }) => {
),
currentPageId: state.entities.pageList.currentPageId,
applicationId: state.entities.pageList.applicationId,
suggestedWidgets,
hasResponse,
};
}, mapDispatchToProps)(
reduxForm<Action, APIFormProps>({

View File

@ -56,6 +56,10 @@ import { getThemeDetails, ThemeMode } from "selectors/themeSelectors";
import ToggleModeButton from "pages/Editor/ToggleModeButton";
import TooltipComponent from "components/ads/Tooltip";
import moment from "moment/moment";
import { Colors } from "constants/Colors";
import { snipingModeSelector } from "selectors/editorSelectors";
import { setSnipingMode as setSnipingModeAction } from "actions/propertyPaneActions";
import { useLocation } from "react-router";
const HeaderWrapper = styled(StyledHeader)`
width: 100%;
@ -140,6 +144,29 @@ const StyledDeployButton = styled(Button)`
padding: ${(props) => props.theme.spaces[2]}px;
`;
const BindingBanner = styled.div`
position: fixed;
width: 199px;
height: 36px;
left: 50%;
top: ${(props) => props.theme.smallHeaderHeight};
transform: translate(-50%, 0);
text-align: center;
background: ${Colors.DANUBE};
color: ${Colors.WHITE};
font-weight: 500;
font-size: 15px;
line-height: 20px;
/* Depth: 01 */
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.1);
z-index: 9999;
`;
type EditorHeaderProps = {
pageSaveError?: boolean;
pageName?: string;
@ -167,8 +194,9 @@ export function EditorHeader(props: EditorHeaderProps) {
pageSaveError,
publishApplication,
} = props;
const location = useLocation();
const dispatch = useDispatch();
const isSnipingMode = useSelector(snipingModeSelector);
const isSavingName = useSelector(getIsSavingAppName);
const isErroredSavingName = useSelector(getIsErroredSavingAppName);
const applicationList = useSelector(getApplicationList);
@ -177,6 +205,15 @@ export function EditorHeader(props: EditorHeaderProps) {
"",
);
useEffect(() => {
if (window.location.href) {
const searchParams = new URL(window.location.href).searchParams;
const isSnipingMode = searchParams.get("isSnipingMode");
const updatedIsSnipingMode = isSnipingMode === "true";
dispatch(setSnipingModeAction(updatedIsSnipingMode));
}
}, [location]);
const findLastUpdatedTimeMessage = () => {
setLastUpdatedTimeMessage(
lastUpdatedTime
@ -360,6 +397,11 @@ export function EditorHeader(props: EditorHeaderProps) {
</HeaderSection>
<OnboardingHelper />
<GlobalSearch />
{isSnipingMode && (
<BindingBanner className="t--sniping-mode-banner">
Select a widget to bind
</BindingBanner>
)}
</HeaderWrapper>
</ThemeProvider>
);

View File

@ -42,7 +42,11 @@ export const useNavigateToWidget = () => {
selectWidget(widgetId, false);
navigateToCanvas(params, window.location.pathname, pageId, widgetId);
flashElementById(widgetId);
dispatch(forceOpenPropertyPane(widgetId));
// Navigating to a widget from query pane seems to make the property pane
// appear below the entity explorer hence adding a timeout here
setTimeout(() => {
dispatch(forceOpenPropertyPane(widgetId));
}, 0);
};
const navigateToWidget = useCallback(

View File

@ -23,6 +23,7 @@ import { getSelectedText } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { WIDGETS_SEARCH_ID } from "constants/Explorer";
import { setCommentMode as setCommentModeAction } from "actions/commentActions";
import { resetSnipingMode as resetSnipingModeAction } from "actions/propertyPaneActions";
import { showDebugger } from "actions/debuggerActions";
import { setCommentModeInUrl } from "pages/Editor/ToggleModeButton";
@ -35,6 +36,7 @@ type Props = {
cutSelectedWidget: () => void;
toggleShowGlobalSearchModal: () => void;
resetCommentMode: () => void;
resetSnipingMode: () => void;
openDebugger: () => void;
closeProppane: () => void;
closeTableFilterProppane: () => void;
@ -189,6 +191,7 @@ class GlobalHotKeys extends React.Component<Props> {
label="Deselect all Widget"
onKeyDown={(e: any) => {
this.props.resetCommentMode();
this.props.resetSnipingMode();
this.props.deselectAllWidgets();
this.props.closeProppane();
this.props.closeTableFilterProppane();
@ -199,7 +202,11 @@ class GlobalHotKeys extends React.Component<Props> {
combo="v"
global
label="Edit Mode"
onKeyDown={this.props.resetCommentMode}
onKeyDown={(e: any) => {
this.props.resetCommentMode();
this.props.resetSnipingMode();
e.preventDefault();
}}
/>
<Hotkey
combo="c"
@ -239,6 +246,7 @@ const mapDispatchToProps = (dispatch: any) => {
cutSelectedWidget: () => dispatch(cutWidget()),
toggleShowGlobalSearchModal: () => dispatch(toggleShowGlobalSearchModal()),
resetCommentMode: () => dispatch(setCommentModeAction(false)),
resetSnipingMode: () => dispatch(resetSnipingModeAction()),
openDebugger: () => dispatch(showDebugger()),
closeProppane: () => dispatch(closePropertyPane()),
closeTableFilterProppane: () => dispatch(closeTableFilterPane()),

View File

@ -20,14 +20,13 @@ import ActionNameEditor from "components/editorComponents/ActionNameEditor";
import DropdownField from "components/editorComponents/form/fields/DropdownField";
import { ControlProps } from "components/formControls/BaseControl";
import ActionSettings from "pages/Editor/ActionSettings";
import { addTableWidgetFromQuery } from "actions/widgetActions";
import { OnboardingStep } from "constants/OnboardingConstants";
import Boxed from "components/editorComponents/Onboarding/Boxed";
import log from "loglevel";
import Callout from "components/ads/Callout";
import { Variant } from "components/ads/common";
import Text, { TextType } from "components/ads/Text";
import styled, { getTypographyByKey } from "constants/DefaultTheme";
import styled from "constants/DefaultTheme";
import { TabComponent } from "components/ads/Tabs";
import AdsIcon from "components/ads/Icon";
import { Classes } from "components/ads/common";
@ -59,6 +58,8 @@ import { ExplorerURLParams } from "../Explorer/helpers";
import MoreActionsMenu from "../Explorer/Actions/MoreActionsMenu";
import Button, { Size } from "components/ads/Button";
import { thinScrollbar } from "constants/DefaultTheme";
import ActionRightPane from "components/editorComponents/ActionRightPane";
import { SuggestedWidget } from "api/ActionAPI";
import { getActionTabsInitialIndex } from "selectors/editorSelectors";
const QueryFormContainer = styled.form`
@ -128,26 +129,9 @@ const SettingsWrapper = styled.div`
${thinScrollbar};
`;
const GenerateWidgetButton = styled.a`
display: flex;
align-items: center;
position: absolute;
right: 30px;
top: 8px;
${(props) => getTypographyByKey(props, "h5")}
color: #716e6e;
&& {
margin: 0;
}
&:hover {
text-decoration: none;
color: #716e6e;
}
`;
const ResultsCount = styled.div`
position: absolute;
right: 180px;
right: 13px;
top: 8px;
color: #716e6e;
`;
@ -165,7 +149,8 @@ const DocumentationLink = styled.a`
const SecondaryWrapper = styled.div`
display: flex;
flex-direction: column;
height: calc(100% - 50px);
flex: 1;
overflow: hidden;
`;
const HelpSection = styled.div``;
@ -203,7 +188,9 @@ const ErrorContainer = styled.div`
height: 100%;
width: 100%;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
padding-top: 10px;
flex-direction: column;
& > .${Classes.ICON} {
@ -351,6 +338,19 @@ const InlineButton = styled(Button)`
margin: 0 4px;
`;
const Wrapper = styled.div`
display: flex;
flex-direction: row;
height: calc(100% - 50px);
width: 100%;
`;
const SidebarWrapper = styled.div<{ show: boolean }>`
border: 1px solid #e8e8e8;
display: ${(props) => (props.show ? "flex" : "none")};
width: ${(props) => props.theme.actionSidePane.width}px;
`;
type QueryFormProps = {
onDeleteClick: () => void;
onRunClick: () => void;
@ -363,6 +363,7 @@ type QueryFormProps = {
body: any;
isExecutionSuccess?: boolean;
messages?: Array<string>;
suggestedWidgets?: SuggestedWidget[];
};
runErrorMessage: string | undefined;
location: {
@ -439,9 +440,6 @@ export function EditorJSONtoForm(props: Props) {
const isTableResponse = responseType === "TABLE";
const dispatch = useDispatch();
const onAddWidget = () => {
dispatch(addTableWidgetFromQuery(actionName));
};
function MenuList(props: MenuListComponentProps<{ children: Node }>) {
return (
@ -675,111 +673,113 @@ export function EditorJSONtoForm(props: Props) {
</OnboardingIndicator>
</ActionsWrapper>
</StyledFormRow>
<SecondaryWrapper>
<TabContainerView>
{documentationLink && (
<DocumentationLink
className="t--datasource-documentation-link"
onClick={(e: React.MouseEvent) => handleDocumentationClick(e)}
>
{"Documentation "}
<StyledOpenDocsIcon icon="document-open" />
</DocumentationLink>
)}
<TabComponent
tabs={[
{
key: "query",
title: "Query",
panelComponent: (
<SettingsWrapper>
{editorConfig && editorConfig.length > 0 ? (
editorConfig.map(renderEachConfig(formName))
) : (
<>
<ErrorMessage>
An unexpected error occurred
</ErrorMessage>
<Tag
intent="warning"
interactive
minimal
onClick={() => window.location.reload()}
round
>
Refresh
</Tag>
</>
)}
{dataSources.length === 0 && (
<NoDataSourceContainer>
<p className="font18">
Seems like you dont have any Datasources to create
a query
</p>
<EditorButton
filled
icon="plus"
intent="primary"
onClick={() => onCreateDatasourceClick()}
size="small"
text="Add a Datasource"
/>
</NoDataSourceContainer>
)}
</SettingsWrapper>
),
},
{
key: "settings",
title: "Settings",
panelComponent: (
<SettingsWrapper>
<ActionSettings
actionSettingsConfig={settingConfig}
formName={formName}
/>
</SettingsWrapper>
),
},
]}
/>
</TabContainerView>
<TabbedViewContainer ref={panelRef}>
<Resizable
panelRef={panelRef}
setContainerDimensions={(height: number) =>
setTableBodyHeightHeight(height)
}
/>
{output && !!output.length && (
<Boxed step={OnboardingStep.SUCCESSFUL_BINDING}>
<ResultsCount>
<Text type={TextType.P3}>
Result:
<Text type={TextType.H5}>{`${output.length} Record${
output.length > 1 ? "s" : ""
}`}</Text>
</Text>
</ResultsCount>
<GenerateWidgetButton
className="t--add-widget"
onClick={onAddWidget}
<Wrapper>
<SecondaryWrapper>
<TabContainerView>
{documentationLink && (
<DocumentationLink
className="t--datasource-documentation-link"
onClick={(e: React.MouseEvent) => handleDocumentationClick(e)}
>
<AdsIcon name="plus" />
&nbsp;&nbsp;Generate Widget
</GenerateWidgetButton>
</Boxed>
)}
{"Documentation "}
<StyledOpenDocsIcon icon="document-open" />
</DocumentationLink>
)}
<TabComponent
tabs={[
{
key: "query",
title: "Query",
panelComponent: (
<SettingsWrapper>
{editorConfig && editorConfig.length > 0 ? (
editorConfig.map(renderEachConfig(formName))
) : (
<>
<ErrorMessage>
An unexpected error occurred
</ErrorMessage>
<Tag
intent="warning"
interactive
minimal
onClick={() => window.location.reload()}
round
>
Refresh
</Tag>
</>
)}
{dataSources.length === 0 && (
<NoDataSourceContainer>
<p className="font18">
Seems like you dont have any Datasources to
create a query
</p>
<EditorButton
filled
icon="plus"
intent="primary"
onClick={() => onCreateDatasourceClick()}
size="small"
text="Add a Datasource"
/>
</NoDataSourceContainer>
)}
</SettingsWrapper>
),
},
{
key: "settings",
title: "Settings",
panelComponent: (
<SettingsWrapper>
<ActionSettings
actionSettingsConfig={settingConfig}
formName={formName}
/>
</SettingsWrapper>
),
},
]}
/>
</TabContainerView>
<TabComponent
onSelect={onTabSelect}
selectedIndex={selectedIndex}
tabs={responseTabs}
<TabbedViewContainer ref={panelRef}>
<Resizable
panelRef={panelRef}
setContainerDimensions={(height: number) =>
setTableBodyHeightHeight(height)
}
/>
{output && !!output.length && (
<Boxed step={OnboardingStep.SUCCESSFUL_BINDING}>
<ResultsCount>
<Text type={TextType.P3}>
Result:
<Text type={TextType.H5}>{`${output.length} Record${
output.length > 1 ? "s" : ""
}`}</Text>
</Text>
</ResultsCount>
</Boxed>
)}
<TabComponent
onSelect={onTabSelect}
selectedIndex={selectedIndex}
tabs={responseTabs}
/>
</TabbedViewContainer>
</SecondaryWrapper>
<SidebarWrapper show={!!output}>
<ActionRightPane
actionName={actionName}
hasResponse={!!output}
suggestedWidgets={executedQueryData?.suggestedWidgets}
/>
</TabbedViewContainer>
</SecondaryWrapper>
</SidebarWrapper>
</Wrapper>
</QueryFormContainer>
</>
);

View File

@ -134,7 +134,7 @@ const useUpdateCommentMode = async (currentUser?: User) => {
const searchParams = new URL(window.location.href).searchParams;
const isCommentMode = searchParams.get("isCommentMode");
const isCommentsIntroSeen = await getCommentsIntroSeen();
const updatedIsCommentMode = isCommentMode === "true" ? true : false;
const updatedIsCommentMode = isCommentMode === "true";
const notLoggedId = currentUser?.username === ANONYMOUS_USERNAME;

View File

@ -7,6 +7,7 @@ import {
} from "constants/ReduxActionConstants";
import moment from "moment";
import { PageAction } from "constants/AppsmithActionConstants/ActionConstants";
import { CommentsReduxState } from "./commentsReducer/interfaces";
const initialState: EditorReduxState = {
initialized: false,
@ -26,6 +27,7 @@ const initialState: EditorReduxState = {
updatingWidgetName: false,
updateWidgetNameError: false,
},
isSnipingMode: false,
};
const editorReducer = createReducer(initialState, {
@ -172,6 +174,15 @@ const editorReducer = createReducer(initialState, {
state.loadingStates.updateWidgetNameError = true;
return { ...state };
},
[ReduxActionTypes.SET_SNIPING_MODE]: (
state: CommentsReduxState,
action: ReduxAction<boolean>,
) => {
return {
...state,
isSnipingMode: action.payload,
};
},
});
export interface EditorReduxState {
@ -182,6 +193,7 @@ export interface EditorReduxState {
currentPageId?: string;
lastUpdatedTime?: number;
pageActions?: PageAction[][];
isSnipingMode: boolean;
loadingStates: {
saving: boolean;
savingError: boolean;

View File

@ -62,6 +62,7 @@ import {
import history from "utils/history";
import {
API_EDITOR_ID_URL,
BUILDER_PAGE_URL,
INTEGRATION_EDITOR_URL,
INTEGRATION_TABS,
QUERIES_EDITOR_ID_URL,
@ -574,6 +575,22 @@ export function* refactorActionName(
}
}
function* bindDataOnCanvasSaga(
action: ReduxAction<{
queryId: string;
applicationId: string;
pageId: string;
}>,
) {
const { applicationId, pageId, queryId } = action.payload;
history.push(
BUILDER_PAGE_URL(applicationId, pageId, {
isSnipingMode: "true",
bindTo: queryId,
}),
);
}
function* saveActionName(action: ReduxAction<{ id: string; name: string }>) {
// Takes from state, checks if the name isValid, saves
const apiId = action.payload.id;
@ -808,6 +825,7 @@ export function* watchActionSagas() {
takeEvery(ReduxActionTypes.CREATE_ACTION_INIT, createActionSaga),
takeLatest(ReduxActionTypes.UPDATE_ACTION_INIT, updateActionSaga),
takeLatest(ReduxActionTypes.DELETE_ACTION_INIT, deleteActionSaga),
takeLatest(ReduxActionTypes.BIND_DATA_ON_CANVAS, bindDataOnCanvasSaga),
takeLatest(ReduxActionTypes.SAVE_ACTION_NAME_INIT, saveActionName),
takeLatest(ReduxActionTypes.MOVE_ACTION_INIT, moveActionSaga),
takeLatest(ReduxActionTypes.COPY_ACTION_INIT, copyActionSaga),

View File

@ -0,0 +1,169 @@
import { takeLeading, all, put, select } from "redux-saga/effects";
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import history from "../utils/history";
import { BUILDER_PAGE_URL } from "../constants/routes";
import {
getCurrentApplicationId,
getCurrentPageId,
} from "../selectors/editorSelectors";
import { ActionData } from "../reducers/entityReducers/actionsReducer";
import { getCanvasWidgets } from "../selectors/entitiesSelector";
import {
setWidgetDynamicProperty,
updateWidgetPropertyRequest,
} from "../actions/controlActions";
import { RenderModes, WidgetTypes } from "../constants/WidgetConstants";
import { Toaster } from "../components/ads/Toast";
import { Variant } from "../components/ads/common";
import AnalyticsUtil from "../utils/AnalyticsUtil";
export function* bindDataToWidgetSaga(
action: ReduxAction<{
widgetId: string;
}>,
) {
const applicationId = yield select(getCurrentApplicationId);
const pageId = yield select(getCurrentPageId);
// console.log("Binding Data in Saga");
const currentURL = new URL(window.location.href);
const searchParams = currentURL.searchParams;
const queryId = searchParams.get("bindTo");
const currentAction = yield select((state) =>
state.entities.actions.find(
(action: ActionData) => action.config.id === queryId,
),
);
const selectedWidget = (yield select(getCanvasWidgets))[
action.payload.widgetId
];
let propertyPath = "";
let propertyValue: any = "";
let isValidProperty = true;
switch (selectedWidget.type) {
case WidgetTypes.BUTTON_WIDGET:
case WidgetTypes.FORM_BUTTON_WIDGET:
propertyPath = "onClick";
propertyValue = `{{${currentAction.config.name}.run()}}`;
break;
case WidgetTypes.CHECKBOX_WIDGET:
propertyPath = "defaultCheckedState";
propertyValue = !!currentAction.data.body;
break;
case WidgetTypes.DATE_PICKER_WIDGET2:
propertyPath = "defaultDate";
propertyValue = `{{${currentAction.config.name}.data}}`;
// setting default date to `js` mode
yield put(
setWidgetDynamicProperty(action.payload.widgetId, propertyPath, true),
);
break;
case WidgetTypes.FILE_PICKER_WIDGET:
propertyPath = "onFilesSelected";
propertyValue = `{{${currentAction.config.name}.run()}}`;
break;
case WidgetTypes.IFRAME_WIDGET:
propertyPath = "source";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.INPUT_WIDGET:
propertyPath = "defaultText";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.LIST_WIDGET:
propertyPath = "items";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.MAP_WIDGET:
propertyPath = "defaultMarkers";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.RADIO_GROUP_WIDGET:
propertyPath = "options";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.RATE_WIDGET:
propertyPath = "onRateChanged";
propertyValue = `{{${currentAction.config.name}.run()}}`;
break;
case WidgetTypes.RICH_TEXT_EDITOR_WIDGET:
propertyPath = "defaultText";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.DROP_DOWN_WIDGET:
propertyPath = "options";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.SWITCH_WIDGET:
propertyPath = "defaultSwitchState";
propertyValue = !!currentAction.data.body;
break;
case WidgetTypes.TABLE_WIDGET:
propertyPath = "tableData";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.TEXT_WIDGET:
propertyPath = "text";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
case WidgetTypes.VIDEO_WIDGET:
propertyPath = "url";
propertyValue = `{{${currentAction.config.name}.data}}`;
break;
default:
isValidProperty = false;
break;
}
AnalyticsUtil.logEvent("WIDGET_SELECTED_VIA_SNIPING_MODE", {
widgetType: selectedWidget.type,
actionName: currentAction.config.name,
apiId: queryId,
propertyPath,
propertyValue,
});
if (queryId && isValidProperty) {
yield put(
updateWidgetPropertyRequest(
action.payload.widgetId,
propertyPath,
propertyValue,
RenderModes.CANVAS,
),
);
yield put({
type: ReduxActionTypes.SHOW_PROPERTY_PANE,
payload: {
widgetId: action.payload.widgetId,
callForDragOrResize: undefined,
force: true,
},
});
history.replace(BUILDER_PAGE_URL(applicationId, pageId, {}));
} else {
queryId &&
Toaster.show({
text: "Binding on selection is not supported for this type of widget!",
variant: Variant.warning,
});
}
}
function* resetSnipingModeSaga() {
const currentURL = new URL(window.location.href);
const searchParams = currentURL.searchParams;
searchParams.delete("isSnipingMode");
searchParams.delete("bindTo");
history.replace({
...window.location,
pathname: currentURL.pathname,
search: searchParams.toString(),
});
}
export default function* snipingModeSagas() {
yield all([
takeLeading(ReduxActionTypes.BIND_DATA_TO_WIDGET, bindDataToWidgetSaga),
takeLeading(ReduxActionTypes.RESET_SNIPING_MODE, resetSnipingModeSaga),
]);
}

View File

@ -70,9 +70,7 @@ import {
WidgetType,
WidgetTypes,
} from "constants/WidgetConstants";
import WidgetConfigResponse, {
GRID_DENSITY_MIGRATION_V1,
} from "mockResponses/WidgetConfigResponse";
import WidgetConfigResponse from "mockResponses/WidgetConfigResponse";
import {
flushDeletedWidgets,
getCopiedWidgets,
@ -115,7 +113,6 @@ import {
import { getAllPaths } from "workers/evaluationUtils";
import {
createMessage,
ERROR_ADD_WIDGET_FROM_QUERY,
ERROR_WIDGET_COPY_NO_WIDGET_SELECTED,
ERROR_WIDGET_CUT_NO_WIDGET_SELECTED,
WIDGET_COPY,
@ -137,6 +134,7 @@ import {
import { getSelectedWidgets } from "selectors/ui";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
import { widgetSelectionSagas } from "./WidgetSelectionSagas";
function* getChildWidgetProps(
parent: FlattenedWidgetProps,
params: WidgetAddChild,
@ -351,6 +349,7 @@ export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
type: addChildAction.payload.type,
},
});
yield put(updateAndSaveLayout(widgets));
// go up till MAIN_CONTAINER, if there is a operation CHILD_OPERATIONS IN ANY PARENT,
@ -1807,44 +1806,36 @@ function* cutWidgetSaga() {
});
}
function* addTableWidgetFromQuerySaga(action: ReduxAction<string>) {
try {
const columns = 8 * GRID_DENSITY_MIGRATION_V1;
const rows = 7 * GRID_DENSITY_MIGRATION_V1;
const queryName = action.payload;
const widgets = yield select(getWidgets);
const evalTree = yield select(getDataTree);
const widgetName = getNextWidgetName(widgets, "TABLE_WIDGET", evalTree);
function* addSuggestedWidget(action: ReduxAction<Partial<WidgetProps>>) {
const widgetConfig = action.payload;
if (!widgetConfig.type) return;
const defaultConfig = WidgetConfigResponse.config[widgetConfig.type];
const evalTree = yield select(getDataTree);
const widgets = yield select(getWidgets);
const widgetName = getNextWidgetName(widgets, widgetConfig.type, evalTree);
try {
let newWidget = {
type: WidgetTypes.TABLE_WIDGET,
newWidgetId: generateReactKey(),
widgetId: "0",
topRow: 0,
bottomRow: rows,
leftColumn: 0,
rightColumn: columns,
columns,
rows,
parentId: MAIN_CONTAINER_WIDGET_ID,
widgetName,
parentId: "0",
renderMode: RenderModes.CANVAS,
parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
parentColumnSpace: 1,
isLoading: false,
version: 1,
props: {
tableData: `{{${queryName}.data}}`,
dynamicBindingPathList: [{ key: "tableData" }],
},
...defaultConfig,
widgetName,
...widgetConfig,
};
const {
bottomRow,
leftColumn,
rightColumn,
topRow,
} = yield calculateNewWidgetPosition(
newWidget,
newWidget as WidgetProps,
MAIN_CONTAINER_WIDGET_ID,
widgets,
);
@ -1874,26 +1865,16 @@ function* addTableWidgetFromQuerySaga(action: ReduxAction<string>) {
pageId,
newWidget.newWidgetId,
);
yield put({
type: ReduxActionTypes.SELECT_WIDGET_INIT,
payload: { widgetId: newWidget.newWidgetId },
});
yield put(forceOpenPropertyPane(newWidget.newWidgetId));
} catch (error) {
Toaster.show({
text: createMessage(ERROR_ADD_WIDGET_FROM_QUERY),
variant: Variant.danger,
});
console.log(error, "Error");
}
}
export default function* widgetOperationSagas() {
yield fork(widgetSelectionSagas);
yield all([
takeEvery(
ReduxActionTypes.ADD_TABLE_WIDGET_FROM_QUERY,
addTableWidgetFromQuerySaga,
),
takeEvery(ReduxActionTypes.ADD_SUGGESTED_WIDGET, addSuggestedWidget),
takeEvery(ReduxActionTypes.WIDGET_ADD_CHILD, addChildSaga),
takeEvery(ReduxActionTypes.WIDGET_DELETE, deleteSagaInit),
takeEvery(ReduxActionTypes.WIDGET_SINGLE_DELETE, deleteSaga),

View File

@ -15,6 +15,7 @@ import orgSagas from "./OrgSagas";
import importedCollectionsSagas from "./CollectionSagas";
import providersSagas from "./ProvidersSaga";
import curlImportSagas from "./CurlImportSagas";
import snipingModeSagas from "./SnipingModeSagas";
import queryPaneSagas from "./QueryPaneSagas";
import modalSagas from "./ModalSagas";
import batchSagas from "./BatchSagas";
@ -52,6 +53,7 @@ const sagas = [
importedCollectionsSagas,
providersSagas,
curlImportSagas,
snipingModeSagas,
queryPaneSagas,
modalSagas,
batchSagas,

View File

@ -59,6 +59,9 @@ export const getIsPageSaving = (state: AppState) => {
return state.ui.editor.loadingStates.saving || areApisSaving;
};
export const snipingModeSelector = (state: AppState) =>
state.ui.editor?.isSnipingMode;
export const getPageSavingError = (state: AppState) => {
return state.ui.editor.loadingStates.savingError;
};

View File

@ -130,10 +130,13 @@ export type EventName =
| "SLASH_COMMAND"
| "DEBUGGER_NEW_ERROR"
| "DEBUGGER_RESOLVED_ERROR"
| "SELECT_IN_CANVAS_CLICK"
| "WIDGET_SELECTED_VIA_SNIPING_MODE"
| "SUGGESTED_WIDGET_CLICK"
| "ASSOCIATED_ENTITY_CLICK"
| "CREATE_DATA_SOURCE_AUTH_API_CLICK"
| "CONNECT_DATA_CLICK"
| "RESPONSE_TAB_RUN_ACTION_CLICK"
| "ASSOCIATED_ENTITY_CLICK"
| "ASSOCIATED_ENTITY_DROPDOWN_CLICK";
function getApplicationId(location: Location) {

View File

@ -2,15 +2,17 @@ import { useDispatch, useSelector } from "react-redux";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { useCallback, useEffect, useState } from "react";
import { commentModeSelector } from "selectors/commentsSelectors";
import { snipingModeSelector } from "selectors/editorSelectors";
export const useShowPropertyPane = () => {
const dispatch = useDispatch();
const isCommentMode = useSelector(commentModeSelector);
const isSnipingMode = useSelector(snipingModeSelector);
return useCallback(
(widgetId?: string, callForDragOrResize?: boolean, force = false) => {
// Don't show property pane in comment mode
if (isCommentMode) return;
if (isCommentMode || isSnipingMode) return;
dispatch(
// If widgetId is not provided, we don't show the property pane.
@ -27,7 +29,7 @@ export const useShowPropertyPane = () => {
},
);
},
[dispatch, isCommentMode],
[dispatch, isCommentMode, isSnipingMode],
);
};

View File

@ -16,6 +16,7 @@ import {
import React, { Component, ReactNode } from "react";
import { get, memoize } from "lodash";
import DraggableComponent from "components/editorComponents/DraggableComponent";
import SnipeableComponent from "components/editorComponents/SnipeableComponent";
import ResizableComponent from "components/editorComponents/ResizableComponent";
import { WidgetExecuteActionPayload } from "constants/AppsmithActionConstants/ActionConstants";
import PositionedContainer from "components/designSystems/appsmith/PositionedContainer";
@ -246,6 +247,15 @@ abstract class BaseWidget<
makeDraggable(content: ReactNode) {
return <DraggableComponent {...this.props}>{content}</DraggableComponent>;
}
/**
* wraps the widget in a draggable component.
* Note: widget drag can be disabled by setting `dragDisabled` prop to true
*
* @param content
*/
makeSnipeable(content: ReactNode) {
return <SnipeableComponent {...this.props}>{content}</SnipeableComponent>;
}
makePositioned(content: ReactNode) {
const style = this.getPositionStyle();
@ -305,6 +315,8 @@ abstract class BaseWidget<
if (!this.props.resizeDisabled) content = this.makeResizable(content);
content = this.showWidgetName(content);
content = this.makeDraggable(content);
content = this.makeSnipeable(content);
// NOTE: In sniping mode we are not blocking onClick events from PositionWrapper.
content = this.makePositioned(content);
}
return content;

View File

@ -6,6 +6,7 @@ import orgSagas from "../src/sagas/OrgSagas";
import importedCollectionsSagas from "../src/sagas/CollectionSagas";
import providersSagas from "../src/sagas/ProvidersSaga";
import curlImportSagas from "../src/sagas/CurlImportSagas";
import snipingModeSagas from "../src/sagas/SnipingModeSagas";
import queryPaneSagas from "../src/sagas/QueryPaneSagas";
import modalSagas from "../src/sagas/ModalSagas";
import batchSagas from "../src/sagas/BatchSagas";
@ -43,6 +44,7 @@ export const sagasToRunForTests = [
importedCollectionsSagas,
providersSagas,
curlImportSagas,
snipingModeSagas,
queryPaneSagas,
modalSagas,
batchSagas,