Skip to content

Commit

Permalink
fix: sniping mode for module to bind to existing and new widgets (#35072
Browse files Browse the repository at this point in the history
)

## Description
Fixes sniping for module instances. 
For binding to widgets, the module instance id and moduleInstance name
was to be passed to the `BindDataButton` component.

Fixes #31957

## Automation
/ok-to-test tags="@tag.All"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/10056587287>
> Commit: 74ec4dc
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=10056587287&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.All`
> Spec:
> <hr>Tue, 23 Jul 2024 10:37:32 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [x] Yes
- [ ] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Introduced new `apiName` prop in the `ApiResponseView` component for
enhanced flexibility in API response rendering.
- Added `moduleInstanceId` to `BindDataButtonProps` for improved data
binding context.
- Enhanced `QueryResponseTab` components by adding `actionName` prop,
improving functionality and display.
  
- **Bug Fixes**
- Updated logic in `QueryResponseTab` to allow for fallback to
`actionName` when rendering, enhancing adaptability.

- **Tests**
- Added a comprehensive test suite for `bindDataToWidgetSaga` to ensure
expected behavior and integration with Redux state management.

- **Refactor**
- Improved the logic in `bindDataToWidgetSaga` to utilize `actionName`,
streamlining the data binding process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
ashit-rath authored Jul 23, 2024
1 parent cbe1f58 commit d39eb58
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const NoResponse = (props: NoResponseProps) => (
function ApiResponseView(props: Props) {
const {
actionResponse = EMPTY_RESPONSE,
apiName,
currentActionConfig,
disabled,
isRunning,
Expand Down Expand Up @@ -335,7 +336,7 @@ function ApiResponseView(props: Props) {
panelComponent: (
<ResponseTabWrapper>
<ApiResponseMeta
actionName={currentActionConfig?.name}
actionName={apiName || currentActionConfig?.name}
actionResponse={actionResponse}
/>
{Array.isArray(messages) && messages.length > 0 && (
Expand Down
5 changes: 4 additions & 1 deletion app/client/src/pages/Editor/QueryEditor/BindDataButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ function BindDataButton(props: BindDataButtonProps) {
pageId: string;
apiId?: string;
queryId?: string;
moduleInstanceId?: string;
}>();

const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
Expand Down Expand Up @@ -338,7 +339,9 @@ function BindDataButton(props: BindDataButtonProps) {
}
dispatch(
bindDataOnCanvas({
queryId: (params.apiId || params.queryId) as string,
queryId: (params.apiId ||
params.queryId ||
params.moduleInstanceId) as string,
applicationId: applicationId as string,
pageId: params.pageId,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export function EditorJSONtoForm(props: Props) {
title: createMessage(DEBUGGER_RESPONSE),
panelComponent: (
<QueryResponseTab
actionName={actionName}
actionSource={actionSource}
currentActionConfig={currentActionConfig}
isRunning={isRunning}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ function QueryDebuggerTabs({
title: createMessage(DEBUGGER_RESPONSE),
panelComponent: (
<QueryResponseTab
actionName={actionName}
actionSource={actionSource}
currentActionConfig={currentActionConfig}
isRunning={isRunning}
Expand Down
4 changes: 3 additions & 1 deletion app/client/src/pages/Editor/QueryEditor/QueryResponseTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ interface Props {
onRunClick: () => void;
currentActionConfig: Action;
runErrorMessage?: string;
actionName: string;
}

const QueryResponseTab = (props: Props) => {
const {
actionName,
actionSource,
currentActionConfig,
isRunning,
Expand Down Expand Up @@ -270,7 +272,7 @@ const QueryResponseTab = (props: Props) => {
value={selectedControl}
/>
<BindDataButton
actionName={currentActionConfig.name}
actionName={actionName || currentActionConfig.name}
hasResponse={!!actionResponse}
suggestedWidgets={actionResponse?.suggestedWidgets}
/>
Expand Down
104 changes: 104 additions & 0 deletions app/client/src/sagas/SnipingModeSaga.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { ModuleInstance } from "@appsmith/constants/ModuleInstanceConstants";
import { keyBy } from "lodash";
import { testStore } from "store";
import { PostgresFactory } from "test/factories/Actions/Postgres";
import type { Saga } from "redux-saga";
import { runSaga } from "redux-saga";
import { bindDataToWidgetSaga } from "./SnipingModeSagas";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { getModuleInstanceById } from "@appsmith/selectors/moduleInstanceSelectors";
import WidgetFactory from "WidgetProvider/factory";
import TableWidget from "widgets/TableWidget/widget";
import { InputFactory } from "test/factories/Widgets/InputFactory";

jest.mock("@appsmith/selectors/moduleInstanceSelectors", () => ({
...jest.requireActual("@appsmith/selectors/moduleInstanceSelectors"),
getModuleInstanceById: jest.fn(),
}));

describe("SnipingModeSaga", () => {
beforeEach(() => {
jest.restoreAllMocks();
});

it("should check for moduleInstance and use when action is missing", async () => {
const widget = InputFactory.build();
const action = PostgresFactory.build();
const moduleInstance = {
id: "module-instance-id",
name: "ModuleInstance1",
} as ModuleInstance;

(getModuleInstanceById as jest.Mock).mockReturnValue(moduleInstance);
const spy = jest
.spyOn(WidgetFactory, "getWidgetMethods")
.mockImplementation(TableWidget.getMethods);

const store = testStore({
entities: {
...({} as any),
actions: [
{
config: action,
},
],
canvasWidgets: keyBy([widget], "widgetId"),
},
ui: {
...({} as any),
editor: {
snipModeBindTo: "module-instance-id",
},
},
});
const dispatched: any[] = [];

await runSaga(
{
dispatch: (action) => dispatched.push(action),
getState: () => store.getState(),
},
bindDataToWidgetSaga as Saga,
{ payload: { widgetId: widget.widgetId, bindingQuery: "data" } },
).toPromise();

expect(dispatched).toEqual([
{
payload: {
updates: [
{
isDynamic: true,
propertyPath: "tableData",
skipValidation: true,
},
],
widgetId: widget.widgetId,
},
type: ReduxActionTypes.BATCH_SET_WIDGET_DYNAMIC_PROPERTY,
},
{
payload: {
shouldReplay: true,
updates: { modify: { tableData: `{{${moduleInstance.name}.data}}` } },
widgetId: widget.widgetId,
},
type: ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
},
{
payload: { bindTo: undefined, isActive: false },
type: ReduxActionTypes.SET_SNIPING_MODE,
},
{
payload: {
invokedBy: undefined,
pageId: undefined,
payload: [widget.widgetId],
selectionRequestType: "One",
},
type: ReduxActionTypes.SELECT_WIDGET_INIT,
},
]);

spy.mockRestore();
});
});
23 changes: 15 additions & 8 deletions app/client/src/sagas/SnipingModeSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { toast } from "design-system";
import type { PropertyUpdates } from "WidgetProvider/constants";
import type { ModuleInstance } from "@appsmith/constants/ModuleInstanceConstants";
import { getModuleInstanceById } from "@appsmith/selectors/moduleInstanceSelectors";

export function* bindDataToWidgetSaga(
action: ReduxAction<{
Expand All @@ -35,6 +37,14 @@ export function* bindDataToWidgetSaga(
(action: ActionData) => action.config.id === queryId,
),
);
const currentModuleInstance: ModuleInstance | undefined = yield select(
getModuleInstanceById,
queryId,
);

const actionName =
currentAction?.config.name || currentModuleInstance?.name || "";

const widgetState: CanvasWidgetsReduxState = yield select(getCanvasWidgets);
const selectedWidget = widgetState[action.payload.widgetId];

Expand All @@ -47,22 +57,19 @@ export function* bindDataToWidgetSaga(
const { widgetId } = action.payload;

let isValidProperty = true;

// Pranav has an Open PR for this file so just returning for now
if (!currentAction) return;

if (!actionName) return;
const { getSnipingModeUpdates } = WidgetFactory.getWidgetMethods(
selectedWidget.type,
);

let updates: Array<PropertyUpdates> = [];

const oneClickBindingQuery = `{{${currentAction.config.name}.data}}`;
const oneClickBindingQuery = `{{${actionName}.data}}`;

const bindingQuery = action.payload.bindingQuery
? `{{${currentAction.config.name}.${action.payload.bindingQuery}}}`
? `{{${actionName}.${action.payload.bindingQuery}}}`
: oneClickBindingQuery;

let isDynamicPropertyPath = true;

if (bindingQuery === oneClickBindingQuery) {
Expand All @@ -72,13 +79,13 @@ export function* bindDataToWidgetSaga(
if (getSnipingModeUpdates) {
updates = getSnipingModeUpdates?.({
data: bindingQuery,
run: `{{${currentAction.config.name}.run()}}`,
run: `{{${actionName}.run()}}`,
isDynamicPropertyPath,
});

AnalyticsUtil.logEvent("WIDGET_SELECTED_VIA_SNIPING_MODE", {
widgetType: selectedWidget.type,
actionName: currentAction.config.name,
actionName: actionName,
apiId: queryId,
propertyPath: updates?.map((update) => update.propertyPath).toString(),
propertyValue: updates?.map((update) => update.propertyPath).toString(),
Expand Down

0 comments on commit d39eb58

Please sign in to comment.