fix: Disable run button when there are empty fields (#24031)

When an API or SQL query is generated, we disable the run button if no
input has been provided to the URL and BODY fields respectively.


Fixes #23957 


#### Media
> A video or a GIF is preferred. when using Loom, don’t embed because it
looks like it’s a GIF. instead, just link to the video
>
>
#### Type of change
> Please delete options that are not relevant.
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- Chore (housekeeping or task changes that don't impact user perception)
- This change requires a documentation update
>
>
>
## Testing
>
#### How Has This Been Tested?
> Please describe the tests that you ran to verify your changes. Also
list any relevant details for your test configuration.
> Delete anything that is not relevant
- [x] Manual
- [ ] Jest
- [ ] Cypress
>
>
#### Test Plan
> https://github.com/appsmithorg/TestSmith/issues/2412
>
>
#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Test-plan-implementation#speedbreaker-features-to-consider-for-every-change)
have been covered
- [x] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans/_edit#areas-of-interest)
- [x] Test plan has been peer reviewed by project stakeholders and other
QA members
- [x] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
Ayangade Adeoluwa 2023-06-08 10:09:19 +01:00 committed by GitHub
parent 0e0d19b195
commit 1e3a82522e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 119 additions and 19 deletions

View File

@ -28,7 +28,7 @@ describe("Addwidget from Query and bind with other widgets", function () {
.focus()
.type("SELECT * FROM configs LIMIT 10;");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wait(1000);
// Mock the response for this test
cy.intercept("/api/v1/actions/execute", {
fixture: "addWidgetTable-mock",

View File

@ -37,7 +37,7 @@ describe("Check Suggested Widgets Feature in Auto Layout", function () {
.focus()
.type("SELECT * FROM configs LIMIT 10;");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wait(1000);
// Mock the response for this test
cy.intercept("/api/v1/actions/execute", {
fixture: "addWidgetTable-mock",

View File

@ -0,0 +1,33 @@
import { ObjectsRegistry } from "../../../../support/Objects/Registry";
const agHelper = ObjectsRegistry.AggregateHelper,
ee = ObjectsRegistry.EntityExplorer,
apiPage = ObjectsRegistry.ApiPage,
dataSources = ObjectsRegistry.DataSources;
const url = "https://www.google.com";
describe("Block Action Execution when no field is present", () => {
it("1. Ensure API Run button is disabled when no url is present", () => {
apiPage.CreateApi("FirstAPI", "GET");
apiPage.AssertRunButtonDisability(true);
apiPage.EnterURL(url);
apiPage.AssertRunButtonDisability(false);
});
it("1. Ensure Run button is disabled when no SQL body field is present", () => {
let name: any;
dataSources.CreateDataSource("MySql", true, false);
cy.get("@dsName").then(($dsName) => {
name = $dsName;
agHelper.Sleep(1000);
dataSources.NavigateFromActiveDS(name, true);
agHelper.GetNClick(dataSources._templateMenu);
dataSources.EnterQuery("SELECT * from users");
dataSources.AssertRunButtonDisability(false);
dataSources.EnterQuery("");
dataSources.AssertRunButtonDisability(true);
});
});
});

View File

@ -17,11 +17,9 @@ describe("API Panel Test Functionality ", function () {
cy.log("Navigation to API Panel screen successful");
cy.generateUUID().then((uid) => {
cy.CreateAPI(`FirstAPI_${uid}`);
cy.RunAPI();
cy.log("Creation of FirstAPI Action successful");
cy.NavigateToAPI_Panel();
cy.CreateAPI(`SecondAPI_${uid}`);
cy.RunAPI();
cy.CheckAndUnfoldEntityItem("Queries/JS");
cy.log("Creation of SecondAPI Action successful");
cy.get(".t--entity-name").contains("FirstAPI");

View File

@ -32,7 +32,7 @@ describe("Addwidget from Query and bind with other widgets", function () {
.focus()
.type("SELECT * FROM configs LIMIT 10;");
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(500);
cy.wait(1000);
// Mock the response for this test
cy.intercept("/api/v1/actions/execute", {
fixture: "addWidgetTable-mock",

View File

@ -113,10 +113,20 @@ export class ApiPage {
this.CreateApi(apiName, apiVerb, aftDSSaved);
this.EnterURL(url);
//this.agHelper.Sleep(2000);// Added because api name edit takes some time to reflect in api sidebar after the call passes.
cy.get(this._apiRunBtn).should("not.be.disabled");
this.AssertRunButtonDisability();
if (queryTimeout != 10000) this.SetAPITimeout(queryTimeout);
}
AssertRunButtonDisability(disabled = false) {
let query = "";
if (disabled) {
query = "be.disabled";
} else {
query = "not.be.disabled";
}
cy.get(this._apiRunBtn).should(query);
}
EnterURL(url: string) {
this.agHelper.EnterValue(url, {
propFieldName: this._resourceUrl,
@ -397,7 +407,7 @@ export class ApiPage {
this.EnterURL(url);
this.agHelper.AssertAutoSave();
//this.agHelper.Sleep(2000);// Added because api name edit takes some time to reflect in api sidebar after the call passes.
cy.get(this._apiRunBtn).should("not.be.disabled");
this.AssertRunButtonDisability();
if (queryTimeout != 10000) this.SetAPITimeout(queryTimeout);
}

View File

@ -834,6 +834,16 @@ export class DataSources {
}
}
AssertRunButtonDisability(disabled = false) {
let query = "";
if (disabled) {
query = "be.disabled";
} else {
query = "not.be.disabled";
}
cy.get(this._runQueryBtn).should(query);
}
public ReadQueryTableResponse(index: number, timeout = 100) {
//timeout can be sent higher values incase of larger tables
this.agHelper.Sleep(timeout); //Settling time for table!

View File

@ -59,6 +59,7 @@ import { TEMP_DATASOURCE_ID } from "constants/Datasource";
import LazyCodeEditor from "components/editorComponents/LazyCodeEditor";
import { getCodeMirrorNamespaceFromEditor } from "utils/getCodeMirrorNamespace";
import { isDynamicValue } from "utils/DynamicBindingUtils";
import { DEFAULT_DATASOURCE_NAME } from "constants/ApiEditorConstants/ApiEditorConstants";
type ReduxStateProps = {
workspaceId: string;
@ -527,7 +528,7 @@ class EmbeddedDatasourcePathComponent extends React.Component<
evaluatedValue={this.handleEvaluatedValue()}
focusElementName={`${this.props.actionName}.url`}
/>
{datasource && datasource.name !== "DEFAULT_REST_DATASOURCE" && (
{datasource && datasource.name !== DEFAULT_DATASOURCE_NAME && (
<StyledTooltip
id="custom-tooltip"
width={this.state.highlightedElementWidth}

View File

@ -8,6 +8,8 @@ import {
const DEFAULT_METHOD_TYPE = HTTP_METHOD.GET;
export const DEFAULT_DATASOURCE_NAME = "DEFAULT_REST_DATASOURCE";
export const DEFAULT_API_ACTION_CONFIG: ApiActionConfig = {
timeoutInMillisecond: DEFAULT_ACTION_TIMEOUT,
encodeParamsToggle: true,
@ -30,7 +32,7 @@ export const DEFAULT_API_ACTION_CONFIG: ApiActionConfig = {
export const DEFAULT_CREATE_API_CONFIG = {
config: DEFAULT_API_ACTION_CONFIG,
datasource: {
name: "DEFAULT_REST_DATASOURCE",
name: DEFAULT_DATASOURCE_NAME,
},
eventData: {
actionType: "API",

View File

@ -1,10 +1,17 @@
import { PluginPackageName } from "entities/Action";
import { PluginPackageName, PluginName } from "entities/Action";
export const QUERY_BODY_FIELDS = [
"actionConfiguration.body",
"actionConfiguration.formData.body.data",
];
export const SQL_DATASOURCES: Array<string> = [
PluginName.POSTGRES,
PluginName.MS_SQL,
PluginName.MY_SQL,
PluginName.ORACLE,
];
export const PLUGIN_PACKAGE_DBS = [
PluginPackageName.POSTGRES,
PluginPackageName.MONGO,

View File

@ -13,11 +13,6 @@ export enum PluginType {
REMOTE = "REMOTE",
}
// more can be added subsequently.
export enum PluginName {
MONGO = "MongoDB",
}
export enum PluginPackageName {
POSTGRES = "postgres-plugin",
MONGO = "mongo-plugin",
@ -30,6 +25,17 @@ export enum PluginPackageName {
ORACLE = "oracle-plugin",
}
// more can be added subsequently.
export enum PluginName {
MONGO = "MongoDB",
POSTGRES = "PostgreSQL",
MY_SQL = "MySQL",
MS_SQL = "Microsoft SQL Server",
GOOGLE_SHEETS = "Google Sheets",
FIRESTORE = "Firestore",
ORACLE = "Oracle",
}
export enum PaginationType {
NONE = "NONE",
PAGE_NO = "PAGE_NO",
@ -110,6 +116,7 @@ export const isStoredDatasource = (val: any): val is StoredDatasource => {
return true;
};
export interface StoredDatasource {
name?: string;
id: string;
pluginId?: string;
datasourceConfiguration?: { url?: string };

View File

@ -66,6 +66,7 @@ import SearchSnippets from "../../common/SearchSnippets";
import { DocsLink, openDoc } from "../../../constants/DocumentationLinks";
import { getApiPaneConfigSelectedTabIndex } from "selectors/apiPaneSelectors";
import { noop } from "lodash";
import { DEFAULT_DATASOURCE_NAME } from "constants/ApiEditorConstants/ApiEditorConstants";
const Form = styled.form`
position: relative;
@ -554,6 +555,7 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
(state: AppState) => state.entities.actions.map((action) => action.config),
equal,
);
const currentActionConfig: Action | undefined = actions.find(
(action) => action.id === params.apiId || action.id === params.queryId,
);
@ -572,6 +574,20 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
getPlugin(state, pluginId ?? ""),
);
// this gets the url of the current action's datasource
const actionDatasourceUrl =
currentActionConfig?.datasource?.datasourceConfiguration?.url || "";
// this gets the name of the current action's datasource
const actionDatasourceName = currentActionConfig?.datasource.name || "";
// if the url is empty and the action's datasource name is the default datasource name (this means the api does not have a datasource attached)
// or the user does not have permission,
// we block action execution.
const blockExecution =
(!actionDatasourceUrl &&
actionDatasourceName === DEFAULT_DATASOURCE_NAME) ||
!isExecutePermitted;
// Debugger render flag
const showDebugger = useSelector(showDebuggerFlag);
@ -621,7 +637,7 @@ function CommonEditorForm(props: CommonFormPropsWithExtraParams) {
/>
<Button
className="t--apiFormRunBtn"
isDisabled={!isExecutePermitted}
isDisabled={blockExecution}
isLoading={isRunning}
onClick={() => {
onRunClick();

View File

@ -4,7 +4,10 @@ import type { InjectedFormProps } from "redux-form";
import { Tag } from "@blueprintjs/core";
import { isString, noop } from "lodash";
import type { Datasource } from "entities/Datasource";
import { getPluginImages } from "selectors/entitiesSelector";
import {
getPluginImages,
getPluginNameFromId,
} from "selectors/entitiesSelector";
import FormControl from "../FormControl";
import type { Action, QueryAction, SaaSAction } from "entities/Action";
import { SlashCommand } from "entities/Action";
@ -81,7 +84,7 @@ import { getErrorAsString } from "sagas/ActionExecution/errorUtils";
import type { UpdateActionPropertyActionPayload } from "actions/pluginActionActions";
import Guide from "pages/Editor/GuidedTour/Guide";
import { inGuidedTour } from "selectors/onboardingSelectors";
import { EDITOR_TABS } from "constants/QueryEditorConstants";
import { EDITOR_TABS, SQL_DATASOURCES } from "constants/QueryEditorConstants";
import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer";
import { isValidFormConfig } from "reducers/evaluationReducers/formEvaluationReducer";
import {
@ -426,6 +429,19 @@ export function EditorJSONtoForm(props: Props) {
userWorkspacePermissions,
);
// get the current action's plugin name
const currentActionPluginName = useSelector((state: AppState) =>
getPluginNameFromId(state, currentActionConfig?.pluginId || ""),
);
// this gets the url of the current action
const actionBody = currentActionConfig?.actionConfiguration?.body || "";
// if (the body is empty and the action is an sql datasource) or the user does not have permission, block action execution.
const blockExecution =
(!actionBody && SQL_DATASOURCES.includes(currentActionPluginName)) ||
!isExecutePermitted;
// Query is executed even once during the session, show the response data.
if (executedQueryData) {
if (!executedQueryData.isExecutionSuccess) {
@ -912,7 +928,7 @@ export function EditorJSONtoForm(props: Props) {
<Button
className="t--run-query"
data-guided-tour-iid="run-query"
isDisabled={!isExecutePermitted}
isDisabled={blockExecution}
isLoading={isRunning}
onClick={onRunClick}
size="md"