diff --git a/.gitignore b/.gitignore
index ee1d723ac9..b3cb968bc7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@
.env
.vscode/*
+app/client/cypress.env.json
+
# to ignore the node_modeules folder
node_modules
# to ignore the package-lock.json file
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js
index 9eeee1fa41..97668db1dd 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ApiPaneTests/API_Edit_spec.js
@@ -38,7 +38,7 @@ describe("API Panel Test Functionality", function() {
cy.log("Creation of FirstAPI Action successful");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.get(apiwidget.settings).click({ force: true });
- cy.get(apiwidget.confirmBeforeExecute).click();
+ cy.get(apiwidget.confirmBeforeExecute).click({ force: true });
cy.get(apiwidget.runQueryButton).click();
cy.get(".bp3-dialog")
.find("button")
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js
index 18c141b2da..be501f8802 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Binding/BindApi_withPageload_Input_spec.js
@@ -1,6 +1,5 @@
const testdata = require("../../../../fixtures/testdata.json");
const apiwidget = require("../../../../locators/apiWidgetslocator.json");
-const commonlocators = require("../../../../locators/commonlocators.json");
const dsl = require("../../../../fixtures/MultipleInput.json");
const widgetsPage = require("../../../../locators/Widgets.json");
const publish = require("../../../../locators/publishWidgetspage.json");
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js
index e9fc7c4050..a1f6ad57a7 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/Replay/Replay_Editor_spec.js
@@ -4,7 +4,6 @@ const datasource = require("../../../../locators/DatasourcesEditor.json");
const datasourceEditor = require("../../../../locators/DatasourcesEditor.json");
const datasourceFormData = require("../../../../fixtures/datasources.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
-const jsEditorLocators = require("../../../../locators/JSEditor.json");
describe("Undo/Redo functionality", function() {
const modifierKey = Cypress.platform === "darwin" ? "meta" : "ctrl";
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js
index 700b1e808f..bd1198452f 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/ActionExecution/Action_PageOnLoad_spec.js
@@ -29,12 +29,13 @@ describe("API Panel Test Functionality", function() {
});
it("Shows which action failed on action fail.", function() {
+ cy.wait(2000);
cy.NavigateToAPI_Panel();
cy.CreateAPI("PageLoadApi2");
cy.enterDatasourceAndPath("https://abc.com", "users");
cy.WaitAutoSave();
cy.get("li:contains('Settings')").click({ force: true });
- cy.get("[data-cy=executeOnLoad]").click({ force: true });
+ cy.get("[name=executeOnLoad]").click({ force: true });
cy.wait("@setExecuteOnLoad");
@@ -44,6 +45,7 @@ describe("API Panel Test Functionality", function() {
cy.wait("@updateLayout");
cy.reload();
+ cy.wait(3000);
cy.get(commonlocators.toastMsg).contains(
`The action "PageLoadApi2" has failed.`,
);
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/Mongo_Spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/Mongo_Spec.js
index 38302ab97a..53d0caef79 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/Mongo_Spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/Mongo_Spec.js
@@ -35,7 +35,6 @@ describe("Create a query with a mongo datasource, run, save and then delete the
// );
cy.validateNSelectDropdown("Commands", "Find Document(s)", "Raw");
-
cy.get(queryLocators.templateMenu).click();
cy.typeValueNValidate('{"find": "listingsAndReviews","limit": 10}');
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/S3_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/S3_spec.js
index 3af052daa7..9e4b6d31ea 100644
--- a/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/S3_spec.js
+++ b/app/client/cypress/integration/Smoke_TestSuite/ServerSideTests/QueryPane/S3_spec.js
@@ -258,81 +258,81 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications",
cy.deleteQueryUsingContext(); //exeute actions & 200 response is verified in this method
});
- // it("5. Validate Read file command, Verify possible error msgs, run & delete the query", () => {
- // cy.NavigateToActiveDSQueryPane(datasourceName);
- // cy.setQueryTimeout(30000);
- // cy.validateNSelectDropdown("Commands", "List files in bucket", "Read file");
+ it("5. Validate Read file command, Verify possible error msgs, run & delete the query", () => {
+ cy.NavigateToActiveDSQueryPane(datasourceName);
+ cy.setQueryTimeout(30000);
+ cy.validateNSelectDropdown("Commands", "List files in bucket", "Read file");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").should(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(false);
- // expect(response.body.data.body).to.contains(
- // "Mandatory parameter 'Bucket Name' is missing.",
- // );
- // });
- // cy.typeValueNValidate("AutoTest", "Bucket Name");
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").should(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(false);
+ expect(response.body.data.body).to.contains(
+ "Mandatory parameter 'Bucket Name' is missing.",
+ );
+ });
+ cy.typeValueNValidate("AutoTest", "Bucket Name");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").then(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(false);
- // expect(response.body.data.body).to.contains(
- // "Required parameter 'File Path' is missing.",
- // );
- // });
- // cy.typeValueNValidate("Auto", "File Path");
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").then(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(false);
+ expect(response.body.data.body).to.contains(
+ "Required parameter 'File Path' is missing.",
+ );
+ });
+ cy.typeValueNValidate("Auto", "File Path");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").then(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(false);
- // expect(response.body.data.body.split("(")[0].trim()).to.be.oneOf([
- // "The specified bucket does not exist",
- // "The specified bucket is not valid.",
- // ]);
- // });
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").then(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(false);
+ expect(response.body.data.body.split("(")[0].trim()).to.be.oneOf([
+ "The specified bucket does not exist",
+ "The specified bucket is not valid.",
+ ]);
+ });
- // cy.typeValueNValidate("assets-test.appsmith.com", "Bucket Name");
+ cy.typeValueNValidate("assets-test.appsmith.com", "Bucket Name");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").then(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(false);
- // expect(response.body.data.body).to.contain(
- // "The specified key does not exist.",
- // );
- // });
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").then(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(false);
+ expect(response.body.data.body).to.contain(
+ "The specified key does not exist.",
+ );
+ });
- // cy.typeValueNValidate("Autofile", "File Path");
+ cy.typeValueNValidate("Autofile", "File Path");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").then(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(false);
- // expect(response.body.data.body).to.contain(
- // "The specified key does not exist.",
- // );
- // });
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").then(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(false);
+ expect(response.body.data.body).to.contain(
+ "The specified key does not exist.",
+ );
+ });
- // cy.typeValueNValidate("AutoFile", "File Path");
+ cy.typeValueNValidate("AutoFile", "File Path");
- // //Commenting below since below dropdown is removed from Read
- // //cy.validateNSelectDropdown("File Data Type", "Base64", "Text / Binary");
+ //Commenting below since below dropdown is removed from Read
+ //cy.validateNSelectDropdown("File Data Type", "Base64", "Text / Binary");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").then(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(true);
- // expect(response.body.data.body.fileData).to.not.eq(
- // "Hi, this is Automation script adding File!",
- // );
- // });
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").then(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(true);
+ expect(response.body.data.body.fileData).to.not.eq(
+ "Hi, this is Automation script adding File!",
+ );
+ });
- // cy.validateNSelectDropdown("Base64 Encode File - Yes/No", "Yes", "No");
- // cy.onlyQueryRun();
- // cy.wait("@postExecute").then(({ response }) => {
- // expect(response.body.data.isExecutionSuccess).to.eq(true);
- // expect(response.body.data.body.fileData).to.eq(
- // "Hi, this is Automation script adding File!",
- // );
- // });
- // cy.deleteQueryUsingContext(); //exeute actions & 200 response is verified in this method
- // });
+ cy.validateNSelectDropdown("Base64 Encode File - Yes/No", "Yes", "No");
+ cy.onlyQueryRun();
+ cy.wait("@postExecute").then(({ response }) => {
+ expect(response.body.data.isExecutionSuccess).to.eq(true);
+ expect(response.body.data.body.fileData).to.eq(
+ "Hi, this is Automation script adding File!",
+ );
+ });
+ cy.deleteQueryUsingContext(); //exeute actions & 200 response is verified in this method
+ });
it("6. Validate Delete file command for new file, Verify possible error msgs, run & delete the query", () => {
cy.NavigateToActiveDSQueryPane(datasourceName);
@@ -564,7 +564,7 @@ describe("Validate CRUD queries for Amazon S3 along with UI flow verifications",
cy.wait(3000); //waiting for deletion to complete! - else next case fails
});
- it("11. Deletes the datasource", () => {
+ it("13. Deletes the datasource", () => {
cy.NavigateToQueryEditor();
cy.NavigateToActiveTab();
cy.contains(".t--datasource-name", datasourceName).click({ force: true });
diff --git a/app/client/cypress/locators/DatasourcesEditor.json b/app/client/cypress/locators/DatasourcesEditor.json
index 2d23656567..d098db303f 100644
--- a/app/client/cypress/locators/DatasourcesEditor.json
+++ b/app/client/cypress/locators/DatasourcesEditor.json
@@ -38,7 +38,7 @@
"Redshift": ".t--plugin-name:contains('Redshift')",
"AmazonS3": ".t--plugin-name:contains('S3')",
"authType": "[data-cy=authType]",
- "OAuth2": "//div[contains(@class,'option') and text()='OAuth 2.0']",
+ "OAuth2": ".t--dropdown-option:contains('OAuth 2.0')",
"accessTokenUrl": "[data-cy='authentication.accessTokenUrl'] input",
"clienID": "[data-cy='authentication.clientId'] input",
"clientSecret": "[data-cy='authentication.clientSecret'] input",
@@ -47,7 +47,7 @@
"serviceAccCredential": "[data-cy='datasourceConfiguration.authentication.password'] input",
"grantType": "[data-cy='authentication.grantType']",
"authorizationURL":"[data-cy='authentication.authorizationUrl'] input",
- "authorisecode": "//div[contains(@class,'option') and text()='Authorization Code']",
+ "authorisecode": ".t--dropdown-option:contains('Authorization Code')",
"saveAndAuthorize": "button:contains('Save and Authorize')",
"basic": "//div[contains(@class,'option') and text()='Basic']",
"basicUsername": "input[name='authentication.username']",
diff --git a/app/client/cypress/locators/apiWidgetslocator.json b/app/client/cypress/locators/apiWidgetslocator.json
index 8091cea4c3..1bfb88441f 100644
--- a/app/client/cypress/locators/apiWidgetslocator.json
+++ b/app/client/cypress/locators/apiWidgetslocator.json
@@ -52,12 +52,12 @@
"actionlist": ".action div div",
"settings": "li:contains('Settings')",
"headers": "li:contains('Headers')",
- "onPageLoad": "[data-cy=executeOnLoad]",
+ "onPageLoad": "[name=executeOnLoad]",
"renameEntity": ".single-select >div:contains('Edit Name')",
"paramsTab": "//li//span[text()='Params']",
"paramKey": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.key\\.0",
"paramValue": ".t--actionConfiguration\\.queryParameters\\[0\\]\\.value\\.0",
"multipartTypeDropdown":"button:contains('Type')",
- "confirmBeforeExecute": "[data-cy=confirmBeforeExecute]",
+ "confirmBeforeExecute": "[name=confirmBeforeExecute]",
"runQueryButton": ".t--apiFormRunBtn"
}
diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js
index efd38ce7cf..10a7c20666 100644
--- a/app/client/cypress/support/commands.js
+++ b/app/client/cypress/support/commands.js
@@ -390,9 +390,9 @@ Cypress.Commands.add(
"addOauthAuthDetails",
(accessTokenUrl, clientId, clientSecret, authURL) => {
cy.get(datasource.authType).click();
- cy.xpath(datasource.OAuth2).click();
+ cy.get(datasource.OAuth2).click();
cy.get(datasource.grantType).click();
- cy.xpath(datasource.authorisecode).click();
+ cy.get(datasource.authorisecode).click();
cy.get(datasource.accessTokenUrl).type(accessTokenUrl);
cy.get(datasource.clienID).type(clientId);
cy.get(datasource.clientSecret).type(clientSecret);
@@ -3369,16 +3369,18 @@ Cypress.Commands.add(
"validateNSelectDropdown",
(ddTitle, currentValue, newValue) => {
let toChange = false;
- cy.xpath('//div[contains(text(),"' + currentValue + '")]').should(
+ cy.xpath('//span[contains(text(),"' + currentValue + '")]').should(
"exist",
currentValue + " dropdown value not present",
);
if (newValue) toChange = true;
if (toChange) {
cy.xpath(
- "//p[text()='" + ddTitle + "']/following-sibling::div/div",
+ "//p[text()='" +
+ ddTitle +
+ "']/parent::label/following-sibling::div/div/div",
).click(); //to expand the dropdown
- cy.xpath('//div[contains(text(),"' + newValue + '")]')
+ cy.xpath('//span[contains(text(),"' + newValue + '")]')
.last()
.click({ force: true }); //to select the new value
}
@@ -3387,19 +3389,17 @@ Cypress.Commands.add(
Cypress.Commands.add("typeValueNValidate", (valueToType, fieldName = "") => {
if (fieldName) {
- cy.xpath("//p[text()='" + fieldName + "']/following-sibling::div").then(
- ($field) => {
- cy.updateCodeInput($field, valueToType);
- },
- );
+ cy.xpath(
+ "//p[text()='" + fieldName + "']/parent::label/following-sibling::div",
+ ).then(($field) => {
+ cy.updateCodeInput($field, valueToType);
+ });
} else {
cy.xpath("//div[@class='CodeEditorTarget']").then(($field) => {
cy.updateCodeInput($field, valueToType);
});
}
-
cy.EvaluateCurrentValue(valueToType);
-
// cy.xpath("//p[text()='" + fieldName + "']/following-sibling::div//div[@class='CodeMirror-code']//span/span").should((fieldValue) => {
// textF = fieldValue.innerText
// fieldValue.innerText = ""
@@ -3446,26 +3446,22 @@ Cypress.Commands.add(
(fieldName = "", currentValue = "") => {
let toValidate = false;
if (currentValue) toValidate = true;
-
if (fieldName) {
cy.xpath(
"//p[text()='" +
fieldName +
- "']/following-sibling::div//div[@class='CodeMirror-code']",
+ "']/parent::label/following-sibling::div//div[@class='CodeMirror-code']",
).click();
} else {
cy.xpath("//div[@class='CodeMirror-code']").click();
}
-
cy.wait(2000);
const val = cy
.get(commonlocators.evaluatedCurrentValue)
.first()
.should("be.visible")
.invoke("text");
-
if (toValidate) expect(val).to.eq(currentValue);
-
return val;
},
);
diff --git a/app/client/src/assets/icons/form/help-outline.svg b/app/client/src/assets/icons/form/help-outline.svg
new file mode 100644
index 0000000000..bdd8aa1db9
--- /dev/null
+++ b/app/client/src/assets/icons/form/help-outline.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/client/src/components/ads/Checkbox.tsx b/app/client/src/components/ads/Checkbox.tsx
index 37ee3d1441..40d95bb7ee 100644
--- a/app/client/src/components/ads/Checkbox.tsx
+++ b/app/client/src/components/ads/Checkbox.tsx
@@ -12,9 +12,10 @@ export type CheckboxProps = CommonComponentProps & {
info?: string;
backgroundColor?: string;
fill?: boolean;
+ name?: string;
};
-const Checkmark = styled.span<{
+export const Checkmark = styled.span<{
disabled?: boolean;
isChecked?: boolean;
info?: string;
@@ -137,6 +138,7 @@ function Checkbox(props: CheckboxProps) {
) =>
onChangeHandler(e.target.checked)
}
diff --git a/app/client/src/components/ads/Dropdown.tsx b/app/client/src/components/ads/Dropdown.tsx
index 6fa8a7a247..20f3447a0d 100644
--- a/app/client/src/components/ads/Dropdown.tsx
+++ b/app/client/src/components/ads/Dropdown.tsx
@@ -14,6 +14,8 @@ import styled from "constants/DefaultTheme";
import SearchComponent from "components/designSystems/appsmith/SearchComponent";
import { Colors } from "constants/Colors";
import Spinner from "./Spinner";
+import { ReactComponent as Check } from "assets/icons/control/checkmark.svg";
+import { ReactComponent as Close } from "assets/icons/control/remove.svg";
import { replayHighlightClass } from "globalStyles/portals";
import Tooltip from "components/ads/Tooltip";
import { isEllipsisActive } from "utils/helpers";
@@ -44,7 +46,7 @@ export interface DropdownSearchProps {
export interface RenderDropdownOptionType {
index?: number;
- option: DropdownOption;
+ option: DropdownOption | DropdownOption[];
optionClickHandler?: (dropdownOption: DropdownOption) => void;
isSelectedNode?: boolean;
extraProps?: any;
@@ -63,8 +65,9 @@ type RenderOption = ({
export type DropdownProps = CommonComponentProps &
DropdownSearchProps & {
options: DropdownOption[];
- selected: DropdownOption;
+ selected: DropdownOption | DropdownOption[];
onSelect?: DropdownOnSelect;
+ isMultiSelect?: boolean;
width?: string;
height?: string;
showLabelOnly?: boolean;
@@ -91,16 +94,19 @@ export type DropdownProps = CommonComponentProps &
fillOptions?: boolean;
dontUsePortal?: boolean;
hideSubText?: boolean;
+ removeSelectedOption?: DropdownOnSelect;
boundary?: PopperBoundary;
defaultIcon?: IconName;
truncateOption?: boolean; // enabled wrapping and adding tooltip on option item of dropdown menu
};
export interface DefaultDropDownValueNodeProps {
- selected: DropdownOption;
+ selected: DropdownOption | DropdownOption[];
showLabelOnly?: boolean;
+ isMultiSelect?: boolean;
isOpen?: boolean;
hasError?: boolean;
renderNode?: RenderOption;
+ selectedOptionClickHandler?: (option: DropdownOption) => void;
placeholder?: string;
showDropIcon?: boolean;
optionWidth: string;
@@ -108,13 +114,13 @@ export interface DefaultDropDownValueNodeProps {
}
export interface RenderDropdownOptionType {
- option: DropdownOption;
+ option: DropdownOption | DropdownOption[];
optionClickHandler?: (dropdownOption: DropdownOption) => void;
}
export const DropdownContainer = styled.div<{ width: string; height?: string }>`
width: ${(props) => props.width};
- height: ${(props) => props.height || `38px`};
+ min-height: ${(props) => props.height};
position: relative;
span.bp3-popover-target {
display: inline-block;
@@ -146,6 +152,41 @@ const DropdownTriggerWrapper = styled.div<{
}
`;
+const StyledCheckmark = styled(Check)`
+ width: 14px;
+ height: 14px;
+ position: absolute;
+ top: -1px;
+ left: -1px;
+`;
+
+const StyledClose = styled(Close)`
+ width: 24px;
+ height: 24px;
+ padding: 3px;
+ padding-right: 10px;
+ &:hover {
+ background-color: #ebebeb;
+ }
+`;
+const SquareBox = styled.div<{
+ backgroundColor?: string;
+ borderColor?: string;
+}>`
+ width: 14px;
+ height: 14px;
+ box-sizing: border-box;
+ position: relative;
+ margin-right: 10px;
+ background-color: ${(props) =>
+ props.backgroundColor ? props.backgroundColor : "transparent"};
+ border: ${(props) =>
+ props.borderColor
+ ? `1.8px solid ${props.borderColor}`
+ : "1.8px solid #A9A7A7"};
+ border-width: 1.8px;
+`;
+
const Selected = styled.div<{
isOpen: boolean;
disabled?: boolean;
@@ -154,6 +195,7 @@ const Selected = styled.div<{
hasError?: boolean;
selected?: boolean;
isLoading?: boolean;
+ isMultiSelect?: boolean;
}>`
padding: ${(props) => props.theme.spaces[2]}px
${(props) => props.theme.spaces[3]}px;
@@ -170,7 +212,7 @@ const Selected = styled.div<{
align-items: center;
justify-content: space-between;
width: 100%;
- height: ${(props) => props.height};
+ min-height: ${(props) => props.height};
cursor: ${(props) =>
props.disabled || props.isLoading ? "not-allowed" : "pointer"};
${(props) =>
@@ -209,11 +251,11 @@ const Selected = styled.div<{
}
&:hover {
background: ${(props) =>
- props.hasError
- ? Colors.FAIR_PINK
- : props.theme.colors.dropdown.hovered.bg};
- border: 1px solid var(--appsmith-input-focus-border-color);
- }
+ !props.isMultiSelect
+ ? props.hasError
+ ? Colors.FAIR_PINK
+ : props.theme.colors.dropdown.hovered.bg
+ : Colors.WHITE}
`;
const DropdownSelect = styled.div``;
@@ -284,7 +326,6 @@ const OptionWrapper = styled.div<{
align-items: center;
min-height: 36px;
background-color: ${(props) => (props.selected ? Colors.GREEN_3 : null)};
-
&&& svg {
rect {
fill: ${(props) => props.theme.colors.dropdownIconBg};
@@ -384,7 +425,7 @@ const SelectedDropDownHolder = styled.div`
display: flex;
align-items: center;
min-width: 0;
- width: 100%;
+ max-width: 100%;
overflow: hidden;
& ${Text} {
@@ -403,7 +444,6 @@ const SelectedIcon = styled(Icon)`
svg {
height: 18px;
width: 18px;
-
rect {
fill: ${(props) => props.theme.colors.dropdownIconBg};
rx: 0;
@@ -479,26 +519,64 @@ function TooltipWrappedText(
function DefaultDropDownValueNode({
hasError,
hideSubText,
+ isMultiSelect,
optionWidth,
placeholder,
renderNode,
selected,
+ selectedOptionClickHandler,
showDropIcon,
showLabelOnly,
}: DefaultDropDownValueNodeProps) {
- const LabelText = selected
- ? showLabelOnly
- ? selected.label
- : selected.value
- : placeholder
- ? placeholder
- : "Please select a option.";
+ const LabelText =
+ !Array.isArray(selected) && selected
+ ? showLabelOnly
+ ? selected.label
+ : selected.value
+ : placeholder
+ ? placeholder
+ : "Please select a option.";
+
function Label() {
- return hasError ? (
- {LabelText}
- ) : (
- {LabelText}
- );
+ if (isMultiSelect && Array.isArray(selected) && selected.length) {
+ return (
+
+ {selected?.map((s: DropdownOption) => {
+ return (
+
+
+ {s.label}
+
+ {
+ event.stopPropagation();
+ if (selectedOptionClickHandler) {
+ selectedOptionClickHandler(s as DropdownOption);
+ }
+ }}
+ />
+
+ );
+ })}
+
+ );
+ } else
+ return hasError ? (
+ {LabelText}
+ ) : (
+
+ {LabelText}
+
+ );
}
return (
@@ -510,29 +588,33 @@ function DefaultDropDownValueNode({
hasError,
optionWidth,
})
+ ) : isMultiSelect && Array.isArray(selected) && selected.length ? (
+
) : (
- <>
- {selected?.icon ? (
-
- ) : null}
-
- {selected?.subText && !hideSubText ? (
-
- {selected.subText}
-
- ) : null}
- >
+ !Array.isArray(selected) && (
+ <>
+ {selected?.icon ? (
+
+ ) : null}
+
+ {selected?.subText && !hideSubText ? (
+
+ {selected.subText}
+
+ ) : null}
+ >
+ )
)}
);
@@ -542,8 +624,9 @@ interface DropdownOptionsProps extends DropdownProps, DropdownSearchProps {
optionClickHandler: (option: DropdownOption) => void;
renderOption?: RenderOption;
headerLabel?: string;
- selected: DropdownOption;
+ selected: DropdownOption | DropdownOption[];
optionWidth: string;
+ isMultiSelect?: boolean;
}
export function RenderDropdownOptions(props: DropdownOptionsProps) {
@@ -594,12 +677,25 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
optionWidth,
});
}
+ let isSelected = false;
+ if (
+ props.isMultiSelect &&
+ Array.isArray(props.selected) &&
+ props.selected.length
+ ) {
+ isSelected = !!props.selected.find(
+ (selectedOption) => selectedOption.value === option.value,
+ );
+ } else {
+ isSelected =
+ (props.selected as DropdownOption).value === option.value;
+ }
return !option.isSectionHeader ? (
props.optionClickHandler(option)}
- selected={props.selected.value === option.value}
+ selected={isSelected}
>
{option.leftElement && (
{option.leftElement}
@@ -612,7 +708,15 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
size={option.iconSize || IconSize.XL}
/>
) : null}
-
+ {props.isMultiSelect ? (
+ isSelected ? (
+
+
+
+ ) : (
+
+ )
+ ) : null}
{props.showLabelOnly ? (
props.truncateOption ? (
{option.value}
)}
-
{option.subText ? (
{option.subText}
@@ -664,10 +767,13 @@ export default function Dropdown(props: DropdownProps) {
errorMsg = "",
placeholder,
helperText,
+ removeSelectedOption,
hasError,
} = { ...props };
const [isOpen, setIsOpen] = useState(false);
- const [selected, setSelected] = useState(props.selected);
+ const [selected, setSelected] = useState(
+ props.selected,
+ );
const closeIfOpen = () => {
if (isOpen) {
@@ -682,7 +788,21 @@ export default function Dropdown(props: DropdownProps) {
const optionClickHandler = useCallback(
(option: DropdownOption) => {
- setSelected(option);
+ if (props.isMultiSelect) {
+ // Multi select -> typeof selected is array of objects
+ if (!selected) {
+ setSelected([option]);
+ } else {
+ const newOptions: DropdownOption[] = [
+ ...(selected as DropdownOption[]),
+ option,
+ ];
+ setSelected(newOptions);
+ }
+ } else {
+ // Single select -> typeof selected is object
+ setSelected(option);
+ }
setIsOpen(false);
onSelect && onSelect(option.value, option);
option.onSelect && option.onSelect(option.value, option);
@@ -690,6 +810,20 @@ export default function Dropdown(props: DropdownProps) {
[onSelect],
);
+ //Removes selected option
+ const selectedOptionClickHandler = useCallback(
+ (optionToBeRemoved: DropdownOption) => {
+ setIsOpen(false);
+ const selectedOptions = (selected as DropdownOption[]).filter(
+ (option: DropdownOption) => option.value !== optionToBeRemoved.value,
+ );
+ setSelected(selectedOptions);
+ removeSelectedOption &&
+ removeSelectedOption(optionToBeRemoved.value, optionToBeRemoved);
+ },
+ [removeSelectedOption],
+ );
+
const errorFlag = hasError || errorMsg.length > 0;
const disabled = props.disabled || isLoading;
const downIconColor = errorFlag ? Colors.POMEGRANATE2 : Colors.DARK_GRAY;
@@ -734,7 +868,8 @@ export default function Dropdown(props: DropdownProps) {
className={props.className}
disabled={props.disabled}
hasError={errorFlag}
- height={props.height || "38px"}
+ height={props.height || getMinHeight(props.isMultiSelect)}
+ isMultiSelect={props.isMultiSelect}
isOpen={isOpen}
onClick={() => setIsOpen(!isOpen)}
selected={!!selected}
@@ -742,14 +877,15 @@ export default function Dropdown(props: DropdownProps) {
- {}
{isLoading ? (
) : (
@@ -776,7 +912,7 @@ export default function Dropdown(props: DropdownProps) {
@@ -793,6 +929,7 @@ export default function Dropdown(props: DropdownProps) {
{dropdownTrigger}
);
}
+
+function getMinHeight(isMultiSelect?: boolean): string {
+ if (isMultiSelect) return "44px";
+ return "38px";
+}
diff --git a/app/client/src/components/ads/TextInput.tsx b/app/client/src/components/ads/TextInput.tsx
index 103544d04c..a9a85a83a4 100644
--- a/app/client/src/components/ads/TextInput.tsx
+++ b/app/client/src/components/ads/TextInput.tsx
@@ -316,7 +316,7 @@ const TextInput = forwardRef(
setInputValue(inputValue);
const inputValueValidation =
props.validator && props.validator(inputValue);
- if (inputValueValidation) {
+ if (inputValueValidation && inputValueValidation.isValid) {
props.validator && setValidation(inputValueValidation);
return (
inputValueValidation.isValid &&
@@ -410,6 +410,7 @@ const TextInput = forwardRef(
data-cy={props.cypressSelector}
hasLeftIcon={hasLeftIcon}
inputRef={ref}
+ name={props?.name}
onBlur={onBlurHandler}
onChange={memoizedChangeHandler}
onFocus={onFocusHandler}
diff --git a/app/client/src/components/ads/common.tsx b/app/client/src/components/ads/common.tsx
index 0230e4740e..47b77e022e 100644
--- a/app/client/src/components/ads/common.tsx
+++ b/app/client/src/components/ads/common.tsx
@@ -7,6 +7,7 @@ export interface CommonComponentProps {
isLoading?: boolean; //default false
cypressSelector?: string;
className?: string;
+ name?: string;
disabled?: boolean; //default false
}
diff --git a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
index e60a4f03d9..47a17144da 100644
--- a/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
+++ b/app/client/src/components/editorComponents/CodeEditor/styledComponents.ts
@@ -61,7 +61,7 @@ export const EditorWrapper = styled.div<{
top: 0;
`
: `position: relative;`}
- min-height: 35px;
+ min-height: 38px;
height: ${(props) => props.height || "auto"};
background-color: ${(props) => editorBackground(props.editorTheme)};
background-color: ${(props) => props.disabled && "#eef2f5"};
@@ -260,7 +260,7 @@ export const EditorWrapper = styled.div<{
${(props) => {
let height = props.height || "auto";
if (props.size === EditorSize.COMPACT && !props.isFocused) {
- height = props.height || "35px";
+ height = props.height || "38px";
}
return `height: ${height}`;
}}
diff --git a/app/client/src/components/editorComponents/form/fields/StyledFormComponents.tsx b/app/client/src/components/editorComponents/form/fields/StyledFormComponents.tsx
new file mode 100644
index 0000000000..3664c26c9f
--- /dev/null
+++ b/app/client/src/components/editorComponents/form/fields/StyledFormComponents.tsx
@@ -0,0 +1,147 @@
+import React from "react";
+import styled from "styled-components";
+import { Colors } from "constants/Colors";
+import { ControlProps } from "components/formControls/BaseControl";
+
+//Styled help text, intended to be used with Form Fields
+export const StyledFormInfo = styled.span<{ config?: ControlProps }>`
+ display: ${(props) =>
+ //SWITCH and CHECKBOX display label text and form input aligned side by side
+ props?.config?.controlType !== "SWITCH" &&
+ props?.config?.controlType !== "CHECKBOX"
+ ? "block;"
+ : "inline;"}
+ font-weight: normal;
+ color: ${Colors.DOVE_GRAY};
+ font-size: 12px;
+ margin-left: 1px;
+ margin-top: 5px;
+ margin-bottom: 8px;
+`;
+
+const FormSubtitleText = styled.span<{ config?: ControlProps }>`
+display: ${(props) =>
+ //SWITCH and CHECKBOX display label text and form input aligned side by side
+ props?.config?.controlType !== "SWITCH" &&
+ props?.config?.controlType !== "CHECKBOX"
+ ? "block;"
+ : "inline;"}
+font-weight: normal;
+color: ${Colors.DOVE_GRAY};
+font-size: 12px;
+`;
+
+//Styled help text, intended to be used with Form Fields
+const FormInputHelperText = styled.p`
+ color: #858282;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: -0.221538px;
+ margin: 0px;
+`;
+
+//Styled error text, intended to be used with Form Fields
+const FormInputErrorText = styled.p`
+ font-style: normal;
+ font-weight: normal;
+ font-size: 12px;
+ line-height: 16px;
+ letter-spacing: -0.221538px;
+ color: #f22b2b;
+ margin: 8px 0 0 0;
+`;
+
+//Styled anchor tag, intended to be used with Form Fields
+const FormInputAnchor = styled.a`
+ display: block;
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 14px;
+ letter-spacing: 0.8px;
+ color: #6a86ce;
+ margin: 0 0 8px 0;
+ text-transform: uppercase;
+`;
+
+const FormInputSwitchToJsonButton = styled.button`
+ font-weight: 500;
+ font-size: 12px;
+ line-height: 14px;
+ letter-spacing: 0.8px;
+ text-transform: uppercase;
+ color: #6a86ce;
+ margin: 0 0 8px 0;
+ border: none;
+ padding-left: 0px;
+ display: block;
+ cursor: pointer;
+ background-color: #fff;
+`;
+
+//Styled form label tag, intended to be used with Form Fields
+const StyledFormLabel = styled.label<{ config?: ControlProps }>`
+ display: inline-block;
+ // TODO: replace condition with props.config?.dataType === "TOGGLE"
+ // required for large texts in CHECKBOX and SWITCH
+ width: ${(props) => props.config?.customStyles?.width || "auto;"}
+ min-width: ${(props) =>
+ props.config?.controlType === "SWITCH" ||
+ props.config?.controlType === "CHECKBOX"
+ ? "auto;"
+ : "50vh;"}
+ margin-left: ${(props) =>
+ // margin required for CHECKBOX
+ props.config?.controlType === "CHECKBOX" ? "0px;" : "16px;"}
+ font-weight: 400;
+ font-size: 14px;
+ line-height: 16px;
+ letter-spacing: 0.02em;
+ color: ${Colors.CHARCOAL};
+ margin-bottom: ${(props) =>
+ props.config?.controlType === "CHECKBOX" ? "0px;" : "8px;"}
+ &:first-child {
+ margin-left: 0px;
+ }
+ p {
+ display: flex;
+ }
+ .label-icon-wrapper {
+ margin-bottom: 0px;
+ display: flex;
+ align-items: center;
+ }
+ .label-icon-wrapper svg path {
+ fill: #939090;
+ }
+`;
+
+interface FormLabelProps {
+ config?: ControlProps;
+ children: JSX.Element | React.ReactNode;
+}
+
+//Wrapper on styled
+function FormLabel(props: FormLabelProps) {
+ return (
+ {props.children}
+ );
+}
+
+//Wrapper on styled
+function FormInfoText(props: FormLabelProps) {
+ return (
+ {props.children}
+ );
+}
+
+export {
+ FormInputSwitchToJsonButton,
+ FormLabel,
+ FormInputAnchor,
+ FormInputErrorText,
+ FormInputHelperText,
+ FormInfoText,
+ FormSubtitleText,
+};
diff --git a/app/client/src/components/formControls/BaseControl.tsx b/app/client/src/components/formControls/BaseControl.tsx
index 1264fe0c16..7188e9aaf1 100644
--- a/app/client/src/components/formControls/BaseControl.tsx
+++ b/app/client/src/components/formControls/BaseControl.tsx
@@ -41,6 +41,8 @@ export interface ControlProps extends ControlData, ControlFunctions {
export interface ControlData {
id: string;
label: string;
+ displayType?: "UI" | "JSON"; //used for switch to JSON view
+ tooltipText?: string;
configProperty: string;
controlType: ControlType;
propertyValue?: any;
@@ -48,11 +50,19 @@ export interface ControlData {
validationMessage?: string;
validationRegex?: string;
dataType?: InputType;
+ initialValue?: string | boolean | number;
+ info?: string; //helper text
isRequired?: boolean;
conditionals: string;
hidden?: HiddenType;
placeholderText?: string;
schema?: any;
+ errorText?: string;
+ showError?: boolean;
+ encrypted?: boolean;
+ subtitle?: string;
+ url?: string;
+ urlText?: string;
logicalTypes?: string[];
comparisonTypes?: string[];
nestedLevels?: number;
diff --git a/app/client/src/components/formControls/CheckboxControl.tsx b/app/client/src/components/formControls/CheckboxControl.tsx
index d2a7e02fe8..7216afac20 100644
--- a/app/client/src/components/formControls/CheckboxControl.tsx
+++ b/app/client/src/components/formControls/CheckboxControl.tsx
@@ -1,28 +1,53 @@
import React from "react";
-import CheckboxField from "components/editorComponents/form/fields/CheckboxField";
+import Checkbox from "components/ads/Checkbox";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "constants/PropertyControlConstants";
+import {
+ Field,
+ WrappedFieldInputProps,
+ WrappedFieldMetaProps,
+} from "redux-form";
import styled from "styled-components";
-const StyledCheckbox = styled(CheckboxField)`
- &&& {
- font-size: 14px;
- margin-top: 10px;
- }
-`;
+const StyledCheckbox = styled(Checkbox)``;
class CheckboxControl extends BaseControl {
getControlType(): ControlType {
return "CHECKBOX";
}
-
render() {
- const { configProperty, info, label } = this.props;
-
- return ;
+ return (
+
+ );
}
}
+type renderComponentProps = CheckboxControlProps & {
+ input?: WrappedFieldInputProps;
+ meta?: WrappedFieldMetaProps;
+};
+
+function renderComponent(props: renderComponentProps) {
+ const onChangeHandler = (value: boolean) => {
+ props.input && props.input.onChange && props.input.onChange(value);
+ };
+
+ return (
+
+ );
+}
export interface CheckboxControlProps extends ControlProps {
info?: string;
}
diff --git a/app/client/src/components/formControls/DropDownControl.tsx b/app/client/src/components/formControls/DropDownControl.tsx
index c9f4c75b66..c0a7428d39 100644
--- a/app/client/src/components/formControls/DropDownControl.tsx
+++ b/app/client/src/components/formControls/DropDownControl.tsx
@@ -1,126 +1,84 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import styled from "styled-components";
-import { MenuItem } from "@blueprintjs/core";
-import { IItemRendererProps } from "@blueprintjs/select";
-import DropdownField from "components/editorComponents/form/fields/DropdownField";
-import { DropdownOption } from "components/constants";
+import Dropdown, { DropdownOption } from "components/ads/Dropdown";
import { ControlType } from "constants/PropertyControlConstants";
-import { theme } from "constants/DefaultTheme";
-import FormLabel from "components/editorComponents/FormLabel";
-import { Colors } from "constants/Colors";
+import _ from "lodash";
+import {
+ Field,
+ WrappedFieldInputProps,
+ WrappedFieldMetaProps,
+} from "redux-form";
const DropdownSelect = styled.div`
font-size: 14px;
width: 50vh;
`;
-const StyledInfo = styled.span`
- font-weight: normal;
- line-height: normal;
- color: ${Colors.DOVE_GRAY};
- font-size: 12px;
- margin-left: 1px;
-`;
-
-const customSelectStyles = {
- option: (
- styles: { [x: string]: any },
- { isDisabled, isFocused, isSelected }: any,
- ) => {
- return {
- ...styles,
- color: Colors.CODE_GRAY,
- backgroundColor: isDisabled
- ? undefined
- : isSelected
- ? Colors.GREY_3
- : isFocused
- ? Colors.GREY_2
- : undefined,
- ":active": {
- ...styles[":active"],
- backgroundColor:
- !isDisabled &&
- (isSelected ? theme.colors.primaryOld : theme.colors.hover),
- },
- };
- },
-};
-
class DropDownControl extends BaseControl {
render() {
- const {
- configProperty,
- customStyles,
- isDisabled,
- isRequired,
- isSearchable,
- label,
- options,
- subtitle,
- } = this.props;
-
let width = "50vh";
- if (customStyles && customStyles.width) {
- width = customStyles.width;
+ if (this.props.customStyles && this.props?.customStyles?.width) {
+ width = this.props?.customStyles?.width;
}
+
return (
-
-
- {label} {isRequired && "*"}
- {subtitle && (
- <>
-
- {subtitle}
- >
- )}
-
-
-
-
-
+
+
+
);
}
- renderItem = (option: DropdownOption, itemProps: IItemRendererProps) => {
- if (!itemProps.modifiers.matchesPredicate) {
- return null;
- }
- const isSelected: boolean = this.isOptionSelected(option);
- return (
-
- );
- };
-
- isOptionSelected = (selectedOption: DropdownOption) => {
- return selectedOption.value === this.props.propertyValue;
- };
-
getControlType(): ControlType {
return "DROP_DOWN";
}
}
+function renderDropdown(props: {
+ input?: WrappedFieldInputProps;
+ meta?: WrappedFieldMetaProps;
+ props: DropDownControlProps & { width?: string };
+ options: { label: string; value: string }[];
+}): JSX.Element {
+ let selectedValue = props.input?.value;
+ if (_.isUndefined(props.input?.value)) {
+ selectedValue = props?.props?.initialValue;
+ }
+ const selectedOption =
+ props?.options.find(
+ (option: DropdownOption) => option.value === selectedValue,
+ ) || {};
+ return (
+
+ );
+}
+
export interface DropDownControlProps extends ControlProps {
options: DropdownOption[];
placeholderText: string;
propertyValue: string;
subtitle?: string;
+ isMultiSelect?: boolean;
isDisabled?: boolean;
isSearchable?: boolean;
}
diff --git a/app/client/src/components/formControls/DynamicInputTextControl.tsx b/app/client/src/components/formControls/DynamicInputTextControl.tsx
index e52a793cea..ca8a7f464a 100644
--- a/app/client/src/components/formControls/DynamicInputTextControl.tsx
+++ b/app/client/src/components/formControls/DynamicInputTextControl.tsx
@@ -1,7 +1,6 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "constants/PropertyControlConstants";
-import FormLabel from "components/editorComponents/FormLabel";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import { AppState } from "reducers";
import { formValueSelector } from "redux-form";
@@ -45,7 +44,7 @@ export function InputText(props: {
inputType?: INPUT_TEXT_INPUT_TYPES;
customStyles?: any;
}) {
- const { actionName, inputType, isRequired, label, name, placeholder } = props;
+ const { actionName, inputType, name, placeholder } = props;
const dataTreePath = actionPathFromName(actionName, name);
let editorProps = {};
@@ -57,15 +56,18 @@ export function InputText(props: {
};
}
- let customStyle = { width: "50vh", minHeight: "55px" };
+ let customStyle = { width: "50vh", minHeight: "38px" };
if (!!props.customStyles && _.isEmpty(props.customStyles) === false) {
- customStyle = props.customStyles;
+ customStyle = { ...props.customStyles };
+ if (props.customStyles?.width) {
+ customStyle.width = "50vh";
+ }
+ if (props.customStyles?.minHeight) {
+ customStyle.minHeight = "34px";
+ }
}
return (
-
- {label} {isRequired && "*"}
-
- {label}
{showTemplate ? (
{
diff --git a/app/client/src/components/formControls/FieldArrayControl.tsx b/app/client/src/components/formControls/FieldArrayControl.tsx
index 7e95988f25..819adcbfdb 100644
--- a/app/client/src/components/formControls/FieldArrayControl.tsx
+++ b/app/client/src/components/formControls/FieldArrayControl.tsx
@@ -5,7 +5,6 @@ import Icon, { IconSize } from "components/ads/Icon";
import { Classes } from "components/ads/common";
import styled from "styled-components";
import { FieldArray } from "redux-form";
-import FormLabel from "components/editorComponents/FormLabel";
import { ControlProps } from "./BaseControl";
const CenteredIcon = styled(Icon)`
@@ -105,17 +104,14 @@ function NestedComponents(props: any) {
}
export default function FieldArrayControl(props: FieldArrayControlProps) {
- const { configProperty, formName, label, schema } = props;
+ const { configProperty, formName, schema } = props;
return (
- <>
- {label}
-
- >
+
);
}
diff --git a/app/client/src/components/formControls/FilePickerControl.tsx b/app/client/src/components/formControls/FilePickerControl.tsx
index 78592a3b09..a518da3f24 100644
--- a/app/client/src/components/formControls/FilePickerControl.tsx
+++ b/app/client/src/components/formControls/FilePickerControl.tsx
@@ -1,18 +1,20 @@
import * as React from "react";
-import { WrappedFieldProps } from "redux-form";
-import "@uppy/core/dist/style.css";
-import "@uppy/dashboard/dist/style.css";
-import "@uppy/webcam/dist/style.css";
-import { Field } from "redux-form";
+import { useState } from "react";
import styled from "styled-components";
-import Uppy from "@uppy/core";
-import Dashboard from "@uppy/dashboard";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "constants/PropertyControlConstants";
-import FormLabel from "components/editorComponents/FormLabel";
import { BaseButton } from "components/designSystems/appsmith/BaseButton";
import { ButtonVariantTypes } from "components/constants";
import { Colors } from "constants/Colors";
+import FilePickerV2 from "components/ads/FilePickerV2";
+import { FileType, SetProgress } from "components/ads/FilePicker";
+import {
+ Field,
+ WrappedFieldInputProps,
+ WrappedFieldMetaProps,
+} from "redux-form";
+import DialogComponent from "components/ads/DialogComponent";
+import { useEffect, useCallback } from "react";
import { replayHighlightClass } from "globalStyles/portals";
const StyledDiv = styled.div`
@@ -29,7 +31,7 @@ const SelectButton = styled(BaseButton)`
max-width: 59px;
margin: 0 0px;
min-height: 32px;
- border-radius: 0px 4px 4px 0px;
+ border-radius: 0px;
font-weight: bold;
background-color: #fff;
border-color: ${Colors.PRIMARY_ORANGE} !important;
@@ -48,85 +50,92 @@ const SelectButton = styled(BaseButton)`
}
`;
-interface FieldFileInputState {
- text: string;
-}
+const FilePickerWrapper = styled.div`
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+`;
-type Props = WrappedFieldProps;
+type RenderFilePickerProps = FilePickerControlProps & {
+ input?: WrappedFieldInputProps;
+ meta?: WrappedFieldMetaProps;
+ onChange: (event: any) => void;
+};
-class FieldFileInput extends React.Component {
- uppy: any;
+function RenderFilePicker(props: RenderFilePickerProps) {
+ const [isOpen, setIsOpen] = useState(false);
+ const [appFileToBeUploaded, setAppFileToBeUploaded] = useState<{
+ file: File;
+ setProgress: SetProgress;
+ } | null>(null);
- constructor(props: Props) {
- super(props);
- this.refreshUppy();
+ const FileUploader = useCallback(
+ async (file: File, setProgress: SetProgress) => {
+ if (!!file) {
+ setAppFileToBeUploaded({
+ file,
+ setProgress,
+ });
+ } else {
+ setAppFileToBeUploaded(null);
+ }
+ },
+ [],
+ );
- this.state = {
- text: "Select file to upload",
- };
- }
+ const onRemoveFile = useCallback(() => setAppFileToBeUploaded(null), []);
- refreshUppy = () => {
- this.uppy = Uppy({
- id: "test",
- autoProceed: false,
- debug: false,
- restrictions: {
- maxNumberOfFiles: 1,
- },
- }).use(Dashboard, {
- hideUploadButton: true,
- });
- this.uppy.on("file-added", (file: any) => {
- const dslFiles = [];
+ useEffect(() => {
+ if (appFileToBeUploaded?.file) {
const reader = new FileReader();
- reader.readAsDataURL(file.data);
+ reader.readAsDataURL(appFileToBeUploaded?.file);
reader.onloadend = () => {
const base64data = reader.result;
- const newFile = {
- id: file.id,
- base64: base64data,
- blob: file.data,
- };
- dslFiles.push(newFile);
- this.uppy.getPlugin("Dashboard").closeModal();
- this.props.input.onChange({
- name: file.name,
+ props.input?.onChange({
+ name: appFileToBeUploaded?.file.name,
base64Content: base64data,
});
};
- });
- };
+ }
+ }, [appFileToBeUploaded]);
- openModal = () => {
- // this.setState({ isOpen: true });
- this.uppy.getPlugin("Dashboard").openModal();
- };
-
- render() {
- const {
- input: { value },
- } = this.props;
-
- return (
+ return (
+ <>
- {value.name}
+ {props?.input?.value?.name}
{
- this.openModal();
+ setIsOpen(true);
}}
text={"Select"}
/>
- );
- }
+ {isOpen ? (
+ setIsOpen(false)}
+ >
+
+
+
+
+ ) : null}
+ >
+ );
}
-
class FilePickerControl extends BaseControl {
constructor(props: FilePickerControlProps) {
super(props);
@@ -136,16 +145,8 @@ class FilePickerControl extends BaseControl {
}
render() {
- const { configProperty, isRequired, label } = this.props;
-
- return (
- <>
-
- {label} {isRequired && "*"}
-
-
- >
- );
+ const { configProperty } = this.props;
+ return ;
}
getControlType(): ControlType {
diff --git a/app/client/src/components/formControls/FixedKeyInputControl.tsx b/app/client/src/components/formControls/FixedKeyInputControl.tsx
index fe49f77151..bacb47ffe3 100644
--- a/app/client/src/components/formControls/FixedKeyInputControl.tsx
+++ b/app/client/src/components/formControls/FixedKeyInputControl.tsx
@@ -3,7 +3,6 @@ import BaseControl, { ControlProps } from "./BaseControl";
import { InputType } from "components/constants";
import { ControlType } from "constants/PropertyControlConstants";
import TextField from "components/editorComponents/form/fields/TextField";
-import FormLabel from "components/editorComponents/FormLabel";
import styled from "styled-components";
const Wrapper = styled.div`
@@ -12,20 +11,10 @@ const Wrapper = styled.div`
class FixKeyInputControl extends BaseControl {
render() {
- const {
- configProperty,
- dataType,
- fixedKey,
- isRequired,
- label,
- placeholderText,
- } = this.props;
+ const { configProperty, dataType, fixedKey, placeholderText } = this.props;
return (
-
- {label} {isRequired && "*"}
-
{
// Get the value property
diff --git a/app/client/src/components/formControls/InputTextControl.tsx b/app/client/src/components/formControls/InputTextControl.tsx
index b3cefb8842..d17204edc6 100644
--- a/app/client/src/components/formControls/InputTextControl.tsx
+++ b/app/client/src/components/formControls/InputTextControl.tsx
@@ -1,12 +1,15 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
import { ControlType } from "constants/PropertyControlConstants";
-import TextField from "components/editorComponents/form/fields/TextField";
-import FormLabel from "components/editorComponents/FormLabel";
-import { FormIcons } from "icons/FormIcons";
+import TextInput from "components/ads/TextInput";
import { Colors } from "constants/Colors";
import styled from "styled-components";
import { InputType } from "components/constants";
+import {
+ Field,
+ WrappedFieldMetaProps,
+ WrappedFieldInputProps,
+} from "redux-form";
export const StyledInfo = styled.span`
font-weight: normal;
@@ -29,51 +32,52 @@ export function InputText(props: {
encrypted?: boolean;
disabled?: boolean;
}) {
- const {
- dataType,
- disabled,
- encrypted,
- isRequired,
- label,
- name,
- placeholder,
- subtitle,
- } = props;
+ const { dataType, disabled, name, placeholder } = props;
return (
-
- {label} {isRequired && "*"}{" "}
- {encrypted && (
- <>
-
- Encrypted
- >
- )}
- {subtitle && (
- <>
-
- {subtitle}
- >
- )}
-
-
);
}
+function renderComponent(
+ props: {
+ placeholder: string;
+ dataType?: InputType;
+ disabled?: boolean;
+ } & {
+ meta: Partial;
+ input: Partial;
+ },
+) {
+ return (
+
+ );
+}
class InputTextControl extends BaseControl {
render() {
const {
configProperty,
dataType,
disabled,
+ encrypted,
isValid,
label,
placeholderText,
@@ -86,7 +90,7 @@ class InputTextControl extends BaseControl {
{
// Always maintain 1 row
if (props.fields.length < 1) {
for (let i = props.fields.length; i < 1; i += 1) {
- props.fields.push({ [keyName[1]]: "", [valueName[1]]: "" });
+ if (keyName && valueName) {
+ props.fields.push({ [keyName[1]]: "", [valueName[1]]: "" });
+ } else {
+ props.fields.push({ key: "", value: "" });
+ }
}
}
}, [props.fields, keyName, valueName]);
@@ -58,87 +75,96 @@ function KeyValueRow(props: KeyValueArrayProps & WrappedFieldArrayProps) {
const keyFieldValidate = useCallback(
(value: string) => {
- if (value && keyFieldProps.validationRegex) {
- const regex = new RegExp(keyFieldProps.validationRegex);
+ if (value && keyFieldProps?.validationRegex) {
+ const regex = new RegExp(keyFieldProps?.validationRegex);
return regex.test(value) ? undefined : keyFieldProps.validationMessage;
}
return undefined;
},
- [keyFieldProps.validationRegex, keyFieldProps.validationMessage],
+ [keyFieldProps?.validationRegex, keyFieldProps?.validationMessage],
);
-
- if (extraData) {
- isRequired = extraData[0].isRequired || extraData[1].isRequired;
- }
+ const maxLen = props.maxLen;
+ //if maxLen exists apply a check on the length
+ const showAddIcon = (index: number): boolean =>
+ maxLen
+ ? index === props.fields.length - 1 && props.fields.length < maxLen
+ : index === props.fields.length - 1;
return typeof props.fields.getAll() === "object" ? (
<>
{props.fields.map((field: any, index: number) => {
- const otherProps: Record = {};
- if (
- props.actionConfig &&
- props.actionConfig[index].description &&
- props.rightIcon
- ) {
- otherProps.rightIcon = (
-
- );
- }
+ let keyTextFieldName = `${field}.key`;
+ let valueTextFieldName = `${field}.value`;
+
+ if (keyName && Array.isArray(keyName) && keyName?.length)
+ keyTextFieldName = `${field}.${keyName[1]}`;
+
+ if (valueName && Array.isArray(valueName) && valueName?.length)
+ valueTextFieldName = `${field}.${valueName[1]}`;
+
return (
-
+ 0 ? "16px" : "0px" }}
+ >
-
- {extraData && extraData[0].label} {isRequired && "*"}
-
-
{!props.actionConfig && (
-
-
- {extraData && extraData[1].label} {isRequired && "*"}
-
-
-
-
-
- {index === props.fields.length - 1 ? (
+
+
+
+ {showAddIcon(index) ? (
props.fields.push({ key: "", value: "" })}
- style={{ alignSelf: "center" }}
+ onClick={() => {
+ props.fields.push({ key: "", value: "" });
+ }}
+ style={{ marginLeft: "16px", alignSelf: "center" }}
/>
) : (
props.fields.remove(index)}
- style={{ alignSelf: "center" }}
+ style={{ marginLeft: "16px", alignSelf: "center" }}
width={20}
/>
)}
@@ -155,7 +181,6 @@ function KeyValueRow(props: KeyValueArrayProps & WrappedFieldArrayProps) {
? `Value (Type: ${props.actionConfig[index].type})`
: `Value (optional)`
}
- {...otherProps}
/>
)}
@@ -165,7 +190,7 @@ function KeyValueRow(props: KeyValueArrayProps & WrappedFieldArrayProps) {
) : null;
}
-class KeyValueFieldArray extends BaseControl {
+class KeyValueArrayControl extends BaseControl {
render() {
const name = getFieldName(this.props.configProperty);
@@ -174,7 +199,7 @@ class KeyValueFieldArray extends BaseControl {
component={KeyValueRow}
rerenderOnEveryChange={false}
{...this.props}
- name={name[0]}
+ name={name ? name[0] : ""}
/>
);
}
@@ -184,8 +209,8 @@ class KeyValueFieldArray extends BaseControl {
}
}
-const getFieldName = (configProperty: string) => {
- return configProperty.split("[*].");
+const getFieldName = (configProperty: string): string[] | undefined => {
+ if (configProperty) return configProperty.split("[*].");
};
const getType = (dataType: string | undefined) => {
@@ -199,12 +224,34 @@ const getType = (dataType: string | undefined) => {
}
};
-export interface KeyValueArrayProps extends ControlProps {
- name: string;
- label: string;
- rightIcon?: JSXElementConstructor<{ height: number; width: number }>;
- description?: string;
- actionConfig?: any;
+function renderTextInput(
+ props: TextInputProps & {
+ dataType?: "text" | "number" | "password";
+ placeholder?: string;
+ defaultValue: string | number;
+ isRequired: boolean;
+ keyFieldValidate?: (value: string) => { isValid: boolean; message: string };
+ errorMsg?: string;
+ helperText?: string;
+ } & {
+ meta: Partial;
+ input: Partial;
+ },
+): JSX.Element {
+ return (
+
+ );
}
-export default KeyValueFieldArray;
+export default KeyValueArrayControl;
diff --git a/app/client/src/components/formControls/KeyValueInputControl.tsx b/app/client/src/components/formControls/KeyValueInputControl.tsx
index 00e0abca42..c485fad51a 100644
--- a/app/client/src/components/formControls/KeyValueInputControl.tsx
+++ b/app/client/src/components/formControls/KeyValueInputControl.tsx
@@ -6,9 +6,28 @@ import { FormIcons } from "icons/FormIcons";
import BaseControl, { ControlProps, ControlData } from "./BaseControl";
import TextField from "components/editorComponents/form/fields/TextField";
import { ControlType } from "constants/PropertyControlConstants";
-import FormLabel from "components/editorComponents/FormLabel";
import { Colors } from "constants/Colors";
+//TODO: combine it with KeyValueArrayControl and deprecate KeyValueInputControl
+
+type KeyValueRowProps = {
+ name: string;
+ label: string;
+ rightIcon?: JSXElementConstructor<{ height: number; width: number }>;
+ description?: string;
+ actionConfig?: any;
+ extraData?: ControlData[];
+ isRequired?: boolean;
+};
+
+export interface KeyValueInputControlProps extends ControlProps {
+ name: string;
+ label: string;
+ rightIcon?: JSXElementConstructor<{ height: number; width: number }>;
+ description?: string;
+ actionConfig?: any;
+}
+
const FormRowWithLabel = styled.div`
display: flex;
flex: 1;
@@ -18,7 +37,7 @@ const FormRowWithLabel = styled.div`
}
`;
-function KeyValueRow(props: Props & WrappedFieldArrayProps) {
+function KeyValueRow(props: KeyValueRowProps & WrappedFieldArrayProps) {
useEffect(() => {
// Always maintain 1 row
if (props.fields.length < 1) {
@@ -32,21 +51,16 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) {
{typeof props.fields.getAll() === "object" && (
-
- {props.label} {props.isRequired && "*"}
-
{props.fields.map((field: any, index: number) => (
- {/* */}
- {/*
*/}
;
- description?: string;
- actionConfig?: any;
- extraData?: ControlData[];
- isRequired?: boolean;
-};
-
-class KeyValueFieldInput extends BaseControl
{
+class KeyValueFieldInputControl extends BaseControl {
render() {
return (
{
}
}
-export interface KeyValueInputProps extends ControlProps {
- name: string;
- label: string;
- rightIcon?: JSXElementConstructor<{ height: number; width: number }>;
- description?: string;
- actionConfig?: any;
-}
-
-export default KeyValueFieldInput;
+export default KeyValueFieldInputControl;
diff --git a/app/client/src/components/formControls/SwitchControl.tsx b/app/client/src/components/formControls/SwitchControl.tsx
index 3c0157bc36..94e8316d03 100644
--- a/app/client/src/components/formControls/SwitchControl.tsx
+++ b/app/client/src/components/formControls/SwitchControl.tsx
@@ -1,19 +1,29 @@
import React from "react";
import BaseControl, { ControlProps } from "./BaseControl";
-import { StyledSwitch } from "./StyledControls";
+import Toggle from "components/ads/Toggle";
import { ControlType } from "constants/PropertyControlConstants";
-import FormLabel from "components/editorComponents/FormLabel";
import { Field, WrappedFieldProps } from "redux-form";
import styled from "styled-components";
-type Props = WrappedFieldProps & {
+type SwitchFieldProps = WrappedFieldProps & {
label: string;
isRequired: boolean;
info: string;
};
-const StyledFormLabel = styled(FormLabel)`
- margin-bottom: 0px;
+const StyledToggle = styled(Toggle)`
+ .slider {
+ margin-left: 10px;
+ width: 40px;
+ height: 20px;
+ }
+ .slider::before {
+ height: 16px;
+ width: 16px;
+ }
+ input:checked + .slider::before {
+ transform: translateX(19px);
+ }
`;
const SwitchWrapped = styled.div`
@@ -26,14 +36,7 @@ const SwitchWrapped = styled.div`
max-width: 60vw;
`;
-const Info = styled.div`
- font-size: 12px;
- opacity: 0.7;
- margin-top: 8px;
- max-width: 60vw;
-`;
-
-export class SwitchField extends React.Component {
+export class SwitchField extends React.Component {
get value() {
const { input } = this.props;
if (typeof input.value !== "string") return !!input.value;
@@ -44,21 +47,18 @@ export class SwitchField extends React.Component {
}
render() {
- const { info, input, isRequired, label } = this.props;
-
return (
-
- {label} {isRequired && "*"}
-
- input.onChange(value)}
+ {
+ this.props.input.onChange(value);
+ }}
+ value={this.value}
/>
- {info && {info} }
);
}
diff --git a/app/client/src/components/formControls/WhereClauseControl.tsx b/app/client/src/components/formControls/WhereClauseControl.tsx
index a8e5699253..e7ae670756 100644
--- a/app/client/src/components/formControls/WhereClauseControl.tsx
+++ b/app/client/src/components/formControls/WhereClauseControl.tsx
@@ -5,7 +5,6 @@ import Icon, { IconSize } from "components/ads/Icon";
import { Classes } from "components/ads/common";
import styled from "styled-components";
import { FieldArray, getFormValues } from "redux-form";
-import FormLabel from "components/editorComponents/FormLabel";
import { ControlProps } from "./BaseControl";
import _ from "lodash";
import { useSelector } from "react-redux";
@@ -64,7 +63,7 @@ const CenteredIcon = styled(Icon)`
// Outer box that houses the whole component
const PrimaryBox = styled.div`
display: flex;
- width: 105vh;
+ width: min-content;
flex-direction: column;
border: 2px solid ${(props) => props.theme.colors.apiPane.dividerBg};
padding: 10px;
@@ -216,7 +215,7 @@ function ConditionBlock(props: any) {
isDisabled = true;
}
return (
-
+
{/* Component to render the joining operator between multiple conditions */}
{/*Hardcoded label to be removed */}
@@ -320,7 +317,6 @@ export default function WhereClauseControl(props: WhereClauseControlProps) {
comparisonTypes, // All possible keys for the comparison
configProperty, // JSON path for the where clause data
formName, // Name of the form, used by redux-form lib to store the data in redux store
- label, // Label for the where clause
logicalTypes, // All possible keys for the logical operators joining multiple conditions
nestedLevels, // Number of nested levels allowed
} = props;
@@ -328,24 +324,21 @@ export default function WhereClauseControl(props: WhereClauseControlProps) {
// Max width is designed in a way that the proportion stays same even after nesting
const maxWidth = 105;
return (
- <>
- {label}
-
- >
+
);
}
diff --git a/app/client/src/icons/FormIcons.tsx b/app/client/src/icons/FormIcons.tsx
index 30237a4f0e..53af94b07d 100644
--- a/app/client/src/icons/FormIcons.tsx
+++ b/app/client/src/icons/FormIcons.tsx
@@ -3,6 +3,7 @@ import { Icon } from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import { IconProps, IconWrapper } from "constants/IconConstants";
import { ReactComponent as InfoIcon } from "assets/icons/form/info-outline.svg";
+import { ReactComponent as HelpIcon } from "assets/icons/form/help-outline.svg";
import { ReactComponent as AddNewIcon } from "assets/icons/form/add-new.svg";
import { ReactComponent as LockIcon } from "assets/icons/form/lock.svg";
@@ -18,6 +19,11 @@ export const FormIcons: {
),
+ HELP_ICON: (props: IconProps) => (
+
+
+
+ ),
HOME_ICON: (props: IconProps) => (
diff --git a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
index 56abab34b4..829f91634d 100644
--- a/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
+++ b/app/client/src/pages/Editor/DataSourceEditor/RestAPIDatasourceForm.tsx
@@ -12,11 +12,8 @@ import {
reduxForm,
} from "redux-form";
import AnalyticsUtil from "utils/AnalyticsUtil";
-import InputTextControl, {
- StyledInfo,
-} from "components/formControls/InputTextControl";
-import KeyValueInputControl from "components/formControls/KeyValueInputControl";
-import DropDownControl from "components/formControls/DropDownControl";
+import FormControl from "pages/Editor/FormControl";
+import { StyledInfo } from "components/formControls/InputTextControl";
import CenteredWrapper from "components/designSystems/appsmith/CenteredWrapper";
import { connect } from "react-redux";
import { AppState } from "reducers";
@@ -127,7 +124,6 @@ export const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
- //margin-top: 16px;
`;
const SaveButtonContainer = styled.div`
@@ -157,14 +153,6 @@ const AuthorizeButton = styled(StyledButton)`
}
`;
-const COMMON_INPUT_PROPS: any = {
- name: "",
- formName: DATASOURCE_REST_API_FORM,
- id: "",
- isValid: false,
- controlType: "",
-};
-
class DatasourceRestAPIEditor extends React.Component {
componentDidMount() {
const search = new URLSearchParams(this.props.location.search);
@@ -380,36 +368,35 @@ class DatasourceRestAPIEditor extends React.Component {
))}
-
+ {this.renderInputTextControlViaFormControl(
+ "url",
+ "URL",
+ "https://example.com",
+ "TEXT",
+ false,
+ true,
+ )}
-
+ {this.renderKeyValueControlViaFormControl(
+ "headers",
+ "Headers",
+ "",
+ false,
+ )}
-
-
-
+
+ {this.renderKeyValueControlViaFormControl(
+ "queryParameters",
+ "Query Parameters",
+ "",
+ false,
+ )}
- {
label: "No",
value: false,
},
- ]}
- placeholderText=""
- propertyValue=""
- subtitle="Header key: X-APPSMITH-SIGNATURE"
- />
+ ],
+ "Send Appsmith signature header",
+ "",
+ true,
+ "Header key: X-APPSMITH-SIGNATURE",
+ )}
{formData.isSendSessionEnabled && (
-
+ {this.renderInputTextControlViaFormControl(
+ "sessionSignatureKey",
+ "Session Details Signature Key",
+ "",
+ "TEXT",
+ false,
+ false,
+ )}
)}
- {
label: "Bearer Token",
value: AuthType.bearerToken,
},
- ]}
- placeholderText=""
- propertyValue=""
- />
+ ],
+ "Authentication Type",
+ "",
+ false,
+ "",
+ )}
{this.renderAuthFields()}
>
@@ -497,28 +487,29 @@ class DatasourceRestAPIEditor extends React.Component {
return (
<>
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.label",
+ "Key",
+ "api_key",
+ "TEXT",
+ false,
+ false,
+ )}
-
-
+
+ {this.renderInputTextControlViaFormControl(
+ "authentication.value",
+ "Value",
+ "value",
+ "TEXT",
+ true,
+ false,
+ )}
-
-
+ {this.renderDropdownControlViaFormControl(
+ "authentication.addTo",
+ [
{
label: "Query Params",
value: ApiKeyAuthType.QueryParams,
@@ -527,21 +518,25 @@ class DatasourceRestAPIEditor extends React.Component {
label: "Header",
value: ApiKeyAuthType.Header,
},
- ]}
- placeholderText=""
- propertyValue=""
- />
+ ],
+ "Add To",
+ "",
+ false,
+ "",
+ )}
{_.get(authentication, "addTo") == "header" && (
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.headerPrefix",
+ "Header Prefix",
+ "eg: Bearer ",
+ "TEXT",
+ false,
+ false,
+ )}
)}
>
@@ -551,13 +546,14 @@ class DatasourceRestAPIEditor extends React.Component {
renderBearerToken = () => {
return (
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.bearerToken",
+ "Bearer Token",
+ "Bearer Token",
+ "TEXT",
+ true,
+ false,
+ )}
);
};
@@ -566,22 +562,24 @@ class DatasourceRestAPIEditor extends React.Component {
return (
<>
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.username",
+ "Username",
+ "Username",
+ "TEXT",
+ false,
+ false,
+ )}
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.password",
+ "Password",
+ "Password",
+ "PASSWORD",
+ true,
+ false,
+ )}
>
);
@@ -603,11 +601,9 @@ class DatasourceRestAPIEditor extends React.Component {
return (
<>
- {
label: "Authorization Code",
value: GrantType.AuthorizationCode,
},
- ]}
- placeholderText=""
- propertyValue=""
- />
+ ],
+ "Grant Type",
+ "",
+ false,
+ "",
+ )}
{content}
>
@@ -633,11 +631,9 @@ class DatasourceRestAPIEditor extends React.Component {
- {
label: "Request URL",
value: false,
},
- ]}
- />
+ ],
+ "Add Access Token To",
+ "",
+ false,
+ "",
+ )}
{_.get(formData.authentication, "isTokenHeader") && (
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.headerPrefix",
+ "Header Prefix",
+ "eg: Bearer ",
+ "TEXT",
+ false,
+ false,
+ )}
)}
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.accessTokenUrl",
+ "Access Token URL",
+ "https://example.com/login/oauth/access_token",
+ "TEXT",
+ false,
+ false,
+ )}
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.clientId",
+ "Client ID",
+ "Client ID",
+ "TEXT",
+ false,
+ false,
+ )}
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.clientSecret",
+ "Client Secret",
+ "Client Secret",
+ "PASSWORD",
+ true,
+ false,
+ )}
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.scopeString",
+ "Scope(s)",
+ "e.g. read, write",
+ "TEXT",
+ false,
+ false,
+ )}
>
);
@@ -706,28 +714,25 @@ class DatasourceRestAPIEditor extends React.Component {
renderOauth2CommonAdvanced = () => {
return (
<>
-
-
-
-
-
+
+ {this.renderInputTextControlViaFormControl(
+ "authentication.audience",
+ "Audience",
+ "https://example.com/oauth/audience",
+ "TEXT",
+ false,
+ false,
+ )}
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.resource",
+ "Resource",
+ "https://example.com/oauth/resource",
+ "TEXT",
+ false,
+ false,
+ )}
>
);
@@ -757,12 +762,14 @@ class DatasourceRestAPIEditor extends React.Component {
-
+ {this.renderInputTextControlViaFormControl(
+ "authentication.authorizationUrl",
+ "Authorization URL",
+ "https://example.com/login/oauth/authorize",
+ "TEXT",
+ false,
+ false,
+ )}
@@ -779,20 +786,19 @@ class DatasourceRestAPIEditor extends React.Component
{
-
+ {this.renderKeyValueControlViaFormControl(
+ "authentication.customAuthenticationParameters",
+ "Custom Authentication Parameters",
+ "",
+ false,
+ )}
- {
label: "Send client credentials in body",
value: false,
},
- ]}
- />
+ ],
+ "Client Authentication",
+ "",
+ false,
+ "",
+ )}
{!_.get(formData.authentication, "isAuthorizationHeader", true) &&
this.renderOauth2CommonAdvanced()}
@@ -824,6 +834,93 @@ class DatasourceRestAPIEditor extends React.Component {
>
);
};
+
+ // All components in formControls must be rendered via FormControl.
+ // FormControl is the common wrapper for all formcontrol components and contains common elements i.e. label, subtitle, helpertext
+ renderInputTextControlViaFormControl(
+ configProperty: string,
+ label: string,
+ placeholderText: string,
+ dataType: "TEXT" | "PASSWORD" | "NUMBER",
+ encrypted: boolean,
+ isRequired: boolean,
+ ) {
+ return (
+
+ );
+ }
+
+ renderDropdownControlViaFormControl(
+ configProperty: string,
+ options: { label: string; value: string | boolean }[],
+ label: string,
+ placeholderText: string,
+ isRequired: boolean,
+ subtitle?: string,
+ ) {
+ const config = {
+ id: "",
+ isValid: false,
+ isRequired: isRequired,
+ controlType: "DROP_DOWN",
+ configProperty: configProperty,
+ options: options,
+ subtitle: subtitle,
+ label: label,
+ conditionals: "",
+ placeholderText: placeholderText,
+ formName: DATASOURCE_REST_API_FORM,
+ };
+ return (
+
+ );
+ }
+
+ renderKeyValueControlViaFormControl(
+ configProperty: string,
+ label: string,
+ placeholderText: string,
+ isRequired: boolean,
+ ) {
+ const config = {
+ id: "",
+ configProperty: configProperty,
+ isValid: false,
+ controlType: "KEYVALUE_ARRAY",
+ placeholderText: placeholderText,
+ label: label,
+ conditionals: "",
+ formName: DATASOURCE_REST_API_FORM,
+ isRequired: isRequired,
+ };
+ return (
+
+ );
+ }
}
const mapStateToProps = (state: AppState, props: any) => {
diff --git a/app/client/src/pages/Editor/FormControl.tsx b/app/client/src/pages/Editor/FormControl.tsx
index e513e86647..4a1abd1e8e 100644
--- a/app/client/src/pages/Editor/FormControl.tsx
+++ b/app/client/src/pages/Editor/FormControl.tsx
@@ -4,6 +4,17 @@ import { isHidden } from "components/formControls/utils";
import { useSelector } from "react-redux";
import { getFormValues } from "redux-form";
import FormControlFactory from "utils/FormControlFactory";
+import Tooltip from "components/ads/Tooltip";
+import {
+ FormLabel,
+ FormInputHelperText,
+ FormInputAnchor,
+ FormInputErrorText,
+ FormInfoText,
+ FormSubtitleText,
+ FormInputSwitchToJsonButton,
+} from "components/editorComponents/form/fields/StyledFormComponents";
+import { FormIcons } from "icons/FormIcons";
interface FormControlProps {
config: ControlProps;
@@ -20,17 +31,137 @@ function FormControl(props: FormControlProps) {
if (hidden) return null;
return (
-
- {FormControlFactory.createControl(
- props.config,
- props.formName,
- props?.multipleConfig,
- )}
+
+ {FormControlFactory.createControl(
+ props.config,
+ props.formName,
+ props?.multipleConfig,
+ )}
+
+
+ );
+}
+
+interface FormConfigProps extends FormControlProps {
+ children: JSX.Element;
+}
+// top contains label, subtitle, urltext, tooltip, dispaly type
+// bottom contains the info and error text
+// props.children will render the form element
+function FormConfig(props: FormConfigProps) {
+ let top, bottom;
+
+ if (props.multipleConfig?.length) {
+ top = (
+
+ {props.multipleConfig?.map((config) => {
+ return renderFormConfigTop({ config });
+ })}
+
+ );
+ bottom = props.multipleConfig?.map((config) => {
+ return renderFormConfigBottom({ config });
+ });
+ return (
+ <>
+ {top}
+ {props.children}
+ {bottom}
+ >
+ );
+ }
+
+ return (
+
+
+ {props.config.controlType === "CHECKBOX" ? (
+ <>
+ {props.children}
+ {renderFormConfigTop({ config: props.config })}
+ >
+ ) : (
+ <>
+ {renderFormConfigTop({ config: props.config })}
+ {props.children}
+ >
+ )}
+
+ {renderFormConfigBottom({ config: props.config })}
);
}
export default FormControl;
+
+function renderFormConfigTop(props: { config: ControlProps }) {
+ const {
+ displayType,
+ encrypted,
+ isRequired,
+ label,
+ subtitle,
+ tooltipText = "",
+ url,
+ urlText,
+ } = { ...props.config };
+ return (
+
+
+
+ {label} {isRequired && "*"}{" "}
+ {encrypted && (
+ <>
+
+
+ Encrypted
+
+ >
+ )}
+ {tooltipText && (
+
+
+
+ )}
+
+ {subtitle && (
+ {subtitle}
+ )}
+
+ {urlText && (
+
+ {urlText}
+
+ )}
+ {displayType && (
+
+ {displayType === "JSON" ? "SWITCH TO GUI" : "SWITCH TO JSON EDITOR"}
+
+ )}
+
+ );
+}
+
+function renderFormConfigBottom(props: { config: ControlProps }) {
+ const { errorText, info, showError } = { ...props.config };
+ return (
+ <>
+ {info &&
{info} }
+ {showError &&
{errorText} }
+ >
+ );
+}
diff --git a/app/client/src/pages/Editor/GeneratePage/components/DataSourceOption.tsx b/app/client/src/pages/Editor/GeneratePage/components/DataSourceOption.tsx
index d836b4fd41..b910b195d0 100644
--- a/app/client/src/pages/Editor/GeneratePage/components/DataSourceOption.tsx
+++ b/app/client/src/pages/Editor/GeneratePage/components/DataSourceOption.tsx
@@ -1,7 +1,10 @@
import React from "react";
import styled from "styled-components";
import { Colors } from "constants/Colors";
-import { RenderDropdownOptionType } from "components/ads/Dropdown";
+import {
+ DropdownOption,
+ RenderDropdownOptionType,
+} from "components/ads/Dropdown";
import { useSelector } from "react-redux";
import { getPluginImages } from "../../../../selectors/entitiesSelector";
import { Classes } from "../../../../components/ads/common";
@@ -101,13 +104,14 @@ function DataSourceOption({
optionClickHandler,
optionWidth,
}: DataSourceOptionType) {
- const { label } = dropdownOption;
+ const { label } = dropdownOption as DropdownOption;
const { routeToCreateNewDatasource = () => null } = extraProps;
const pluginImages = useSelector(getPluginImages);
const isConnectNewDataSourceBtn =
- CONNECT_NEW_DATASOURCE_OPTION_ID === dropdownOption.id;
+ CONNECT_NEW_DATASOURCE_OPTION_ID === (dropdownOption as DropdownOption).id;
- const isSupportedForTemplate = dropdownOption.data.isSupportedForTemplate;
+ const isSupportedForTemplate = (dropdownOption as DropdownOption).data
+ .isSupportedForTemplate;
const isNotSupportedDatasource =
!isSupportedForTemplate && !isSelectedNode && !isConnectNewDataSourceBtn;
@@ -127,7 +131,7 @@ function DataSourceOption({
className="t--dropdown-option"
data-cy={optionCypressSelector}
disabled={isNotSupportedDatasource}
- key={dropdownOption.id}
+ key={(dropdownOption as DropdownOption).id}
onClick={() => {
if (isNotSupportedDatasource) {
return;
@@ -135,7 +139,7 @@ function DataSourceOption({
if (isConnectNewDataSourceBtn) {
routeToCreateNewDatasource(dropdownOption);
} else if (optionClickHandler) {
- optionClickHandler(dropdownOption);
+ optionClickHandler(dropdownOption as DropdownOption);
}
}}
selected={isSelectedNode}
@@ -149,12 +153,14 @@ function DataSourceOption({
width={20}
/>
- ) : pluginImages[dropdownOption.data.pluginId] ? (
+ ) : pluginImages[(dropdownOption as DropdownOption).data.pluginId] ? (
) : null}
diff --git a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx
index d9785886ae..bc2a8e44ca 100644
--- a/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx
+++ b/app/client/src/pages/Editor/GeneratePage/components/GeneratePageForm/GeneratePageForm.tsx
@@ -629,7 +629,7 @@ function GeneratePageForm() {
cypressSelector="t--datasource-dropdown-option"
extraProps={{ routeToCreateNewDatasource }}
isSelectedNode={isSelectedNode}
- key={option.id}
+ key={(option as DropdownOption).id}
option={option}
optionClickHandler={optionClickHandler}
optionWidth={DROPDOWN_DIMENSION.WIDTH}
diff --git a/app/client/src/pages/Editor/gitSync/components/OptionSelector.tsx b/app/client/src/pages/Editor/gitSync/components/OptionSelector.tsx
index 26a75a76e6..c4facc2590 100644
--- a/app/client/src/pages/Editor/gitSync/components/OptionSelector.tsx
+++ b/app/client/src/pages/Editor/gitSync/components/OptionSelector.tsx
@@ -23,7 +23,7 @@ function SelectedValueNode(props: DefaultDropDownValueNodeProps) {
const { selected } = props;
return (
- {selected.label}
+ {(selected as DropdownOption).label}
);
diff --git a/app/client/src/utils/FormControlRegistry.tsx b/app/client/src/utils/FormControlRegistry.tsx
index d9180d3f82..43fd543fc4 100644
--- a/app/client/src/utils/FormControlRegistry.tsx
+++ b/app/client/src/utils/FormControlRegistry.tsx
@@ -13,10 +13,10 @@ import SwitchControl, {
SwitchControlProps,
} from "components/formControls/SwitchControl";
import KeyValueArrayControl, {
- KeyValueArrayProps,
+ KeyValueArrayControlProps,
} from "components/formControls/KeyValueArrayControl";
import KeyValueInputControl, {
- KeyValueInputProps,
+ KeyValueInputControlProps,
} from "components/formControls/KeyValueInputControl";
import FilePickerControl, {
FilePickerControlProps,
@@ -30,7 +30,6 @@ import CheckboxControl, {
import DynamicInputTextControl, {
DynamicInputControlProps,
} from "components/formControls/DynamicInputTextControl";
-import InputNumberControl from "components/formControls/InputNumberControl";
import FieldArrayControl, {
FieldArrayControlProps,
} from "components/formControls/FieldArrayControl";
@@ -52,6 +51,7 @@ class FormControlRegistry {
buildPropertyControl(
controlProps: FixedKeyInputControlProps,
): JSX.Element {
+ //TODO: may not be in use
return
;
},
});
@@ -66,17 +66,23 @@ class FormControlRegistry {
},
});
FormControlFactory.registerControlBuilder("KEYVALUE_ARRAY", {
- buildPropertyControl(controlProps: KeyValueArrayProps): JSX.Element {
+ buildPropertyControl(
+ controlProps: KeyValueArrayControlProps,
+ ): JSX.Element {
return
;
},
});
FormControlFactory.registerControlBuilder("FILE_PICKER", {
buildPropertyControl(controlProps: FilePickerControlProps): JSX.Element {
+ //used by redshift datasource
return
;
},
});
FormControlFactory.registerControlBuilder("KEY_VAL_INPUT", {
- buildPropertyControl(controlProps: KeyValueInputProps): JSX.Element {
+ //TODO: may not be in use, replace it with KeyValueArrayControl
+ buildPropertyControl(
+ controlProps: KeyValueInputControlProps,
+ ): JSX.Element {
return
;
},
});
@@ -94,12 +100,13 @@ class FormControlRegistry {
});
FormControlFactory.registerControlBuilder("CHECKBOX", {
buildPropertyControl(controlProps: CheckboxControlProps): JSX.Element {
+ //used in API datasource form only
return
;
},
});
FormControlFactory.registerControlBuilder("NUMBER_INPUT", {
buildPropertyControl(controlProps: InputControlProps): JSX.Element {
- return
;
+ return
;
},
});
FormControlFactory.registerControlBuilder("ARRAY_FIELD", {