feat: adding ads component for UQI (#8777)

This commit is contained in:
rashmi rai 2021-12-27 17:34:45 +05:30 committed by GitHub
parent 7a0715af7c
commit f6b9abdacf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1329 additions and 771 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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")

View File

@ -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");

View File

@ -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";

View File

@ -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.`,
);

View File

@ -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}');

View File

@ -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 });

View File

@ -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']",

View File

@ -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"
}

View File

@ -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;
},
);

View File

@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 18.5C14.4183 18.5 18 14.9183 18 10.5C18 6.08172 14.4183 2.5 10 2.5C5.58172 2.5 2 6.08172 2 10.5C2 14.9183 5.58172 18.5 10 18.5ZM10.4459 11.9234V11.6155C10.4459 11.0807 10.6459 10.8052 11.3541 10.3893C12.0892 9.95172 12.5 9.37373 12.5 8.54727C12.5 7.35348 11.5108 6.5 10.0297 6.5C8.42432 6.5 7.54324 7.4291 7.5 8.70932H8.77027C8.81351 8.0503 9.26216 7.62897 9.95946 7.62897C10.6405 7.62897 11.0946 8.0341 11.0946 8.59588C11.0946 9.13066 10.8676 9.41695 10.1973 9.82208C9.45135 10.2596 9.13784 10.7458 9.18649 11.556L9.19189 11.9234H10.4459ZM9.87838 14.5C10.4297 14.5 10.7757 14.1597 10.7757 13.6411C10.7757 13.1172 10.4297 12.7768 9.87838 12.7768C9.33784 12.7768 8.98108 13.1172 8.98108 13.6411C8.98108 14.1597 9.33784 14.5 9.87838 14.5Z" fill="#939090"/>
</svg>

After

Width:  |  Height:  |  Size: 911 B

View File

@ -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) {
<input
checked={checked}
disabled={props.disabled}
name={props?.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChangeHandler(e.target.checked)
}

View File

@ -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 ? (
<ErrorLabel>{LabelText}</ErrorLabel>
) : (
<Text type={TextType.P1}>{LabelText}</Text>
);
if (isMultiSelect && Array.isArray(selected) && selected.length) {
return (
<div style={{ display: "flex", width: "100%", flexWrap: "wrap" }}>
{selected?.map((s: DropdownOption) => {
return (
<div
key={s.value}
style={{
border: "1.2px solid #E0DEDE",
display: "flex",
alignItems: "center",
lineHeight: "19px",
margin: "2px 2px",
}}
>
<span style={{ padding: "3px" }}>
<Text type={TextType.P1}>{s.label}</Text>
</span>
<StyledClose
onClick={(event: any) => {
event.stopPropagation();
if (selectedOptionClickHandler) {
selectedOptionClickHandler(s as DropdownOption);
}
}}
/>
</div>
);
})}
</div>
);
} else
return hasError ? (
<ErrorLabel>{LabelText}</ErrorLabel>
) : (
<span style={{ width: "100%" }}>
<Text type={TextType.P1}>{LabelText}</Text>
</span>
);
}
return (
@ -510,29 +588,33 @@ function DefaultDropDownValueNode({
hasError,
optionWidth,
})
) : isMultiSelect && Array.isArray(selected) && selected.length ? (
<Label />
) : (
<>
{selected?.icon ? (
<SelectedIcon
fillColor={hasError ? Colors.POMEGRANATE2 : selected?.iconColor}
hoverFillColor={
hasError ? Colors.POMEGRANATE2 : selected?.iconColor
}
name={selected.icon}
size={selected.iconSize || IconSize.XL}
/>
) : null}
<Label />
{selected?.subText && !hideSubText ? (
<StyledSubText
className="sub-text"
showDropIcon={showDropIcon}
type={TextType.P1}
>
{selected.subText}
</StyledSubText>
) : null}
</>
!Array.isArray(selected) && (
<>
{selected?.icon ? (
<SelectedIcon
fillColor={hasError ? Colors.POMEGRANATE2 : selected?.iconColor}
hoverFillColor={
hasError ? Colors.POMEGRANATE2 : selected?.iconColor
}
name={selected.icon}
size={selected.iconSize || IconSize.XL}
/>
) : null}
<Label />
{selected?.subText && !hideSubText ? (
<StyledSubText
className="sub-text"
showDropIcon={showDropIcon}
type={TextType.P1}
>
{selected.subText}
</StyledSubText>
) : null}
</>
)
)}
</SelectedDropDownHolder>
);
@ -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 ? (
<OptionWrapper
className="t--dropdown-option"
key={index}
onClick={() => props.optionClickHandler(option)}
selected={props.selected.value === option.value}
selected={isSelected}
>
{option.leftElement && (
<LeftIconWrapper>{option.leftElement}</LeftIconWrapper>
@ -612,7 +708,15 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
size={option.iconSize || IconSize.XL}
/>
) : null}
{props.isMultiSelect ? (
isSelected ? (
<SquareBox backgroundColor="#f86a2b" borderColor="#f86a2b">
<StyledCheckmark />
</SquareBox>
) : (
<SquareBox borderColor="#a9a7a7" />
)
) : null}
{props.showLabelOnly ? (
props.truncateOption ? (
<TooltipWrappedText
@ -635,7 +739,6 @@ export function RenderDropdownOptions(props: DropdownOptionsProps) {
) : (
<Text type={TextType.P1}>{option.value}</Text>
)}
{option.subText ? (
<StyledSubText type={TextType.P3}>
{option.subText}
@ -664,10 +767,13 @@ export default function Dropdown(props: DropdownProps) {
errorMsg = "",
placeholder,
helperText,
removeSelectedOption,
hasError,
} = { ...props };
const [isOpen, setIsOpen] = useState<boolean>(false);
const [selected, setSelected] = useState<DropdownOption>(props.selected);
const [selected, setSelected] = useState<DropdownOption | DropdownOption[]>(
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) {
<SelectedValueNode
hasError={errorFlag}
hideSubText={props.hideSubText}
isMultiSelect={props.isMultiSelect}
optionWidth={dropdownOptionWidth}
placeholder={placeholder}
renderNode={renderOption}
selected={selected}
selectedOptionClickHandler={selectedOptionClickHandler}
showDropIcon={showDropIcon}
showLabelOnly={props.showLabelOnly}
/>
{}
{isLoading ? (
<Spinner size={IconSize.LARGE} />
) : (
@ -776,7 +912,7 @@ export default function Dropdown(props: DropdownProps) {
<DropdownContainer
className={props.containerClassName + " " + replayHighlightClass}
data-cy={props.cypressSelector}
height={props.height || "36px"}
height={getMinHeight(props.isMultiSelect)}
tabIndex={0}
width={dropdownWidth}
>
@ -793,6 +929,7 @@ export default function Dropdown(props: DropdownProps) {
{dropdownTrigger}
<RenderDropdownOptions
{...props}
isMultiSelect={props.isMultiSelect}
optionClickHandler={optionClickHandler}
optionWidth={dropdownOptionWidth}
selected={
@ -805,3 +942,8 @@ export default function Dropdown(props: DropdownProps) {
</DropdownContainer>
);
}
function getMinHeight(isMultiSelect?: boolean): string {
if (isMultiSelect) return "44px";
return "38px";
}

View File

@ -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}

View File

@ -7,6 +7,7 @@ export interface CommonComponentProps {
isLoading?: boolean; //default false
cypressSelector?: string;
className?: string;
name?: string;
disabled?: boolean; //default false
}

View File

@ -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}`;
}}

View File

@ -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 <label/>
function FormLabel(props: FormLabelProps) {
return (
<StyledFormLabel config={props.config}>{props.children}</StyledFormLabel>
);
}
//Wrapper on styled <span/>
function FormInfoText(props: FormLabelProps) {
return (
<StyledFormInfo config={props.config}>{props.children}</StyledFormInfo>
);
}
export {
FormInputSwitchToJsonButton,
FormLabel,
FormInputAnchor,
FormInputErrorText,
FormInputHelperText,
FormInfoText,
FormSubtitleText,
};

View File

@ -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;

View File

@ -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<CheckboxControlProps> {
getControlType(): ControlType {
return "CHECKBOX";
}
render() {
const { configProperty, info, label } = this.props;
return <StyledCheckbox info={info} label={label} name={configProperty} />;
return (
<Field
component={renderComponent}
name={this.props.configProperty}
props={{ ...this.props }}
type="checkbox"
/>
);
}
}
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 (
<StyledCheckbox
isDefaultChecked={props?.input?.checked as boolean}
{...props}
info={undefined}
label={""}
name={props?.input?.name}
onCheckChange={onChangeHandler}
/>
);
}
export interface CheckboxControlProps extends ControlProps {
info?: string;
}

View File

@ -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<DropDownControlProps> {
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 (
<div>
<FormLabel>
{label} {isRequired && "*"}
{subtitle && (
<>
<br />
<StyledInfo>{subtitle}</StyledInfo>
</>
)}
</FormLabel>
<DropdownSelect data-cy={configProperty} style={{ width }}>
<DropdownField
customSelectStyles={customSelectStyles}
isDisabled={isDisabled}
isSearchable={isSearchable}
name={configProperty}
options={options}
placeholder=""
width={width}
/>
</DropdownSelect>
</div>
<DropdownSelect data-cy={this.props.configProperty} style={{ width }}>
<Field
component={renderDropdown}
name={this.props.configProperty}
options={this.props.options}
props={{ ...this.props, width }}
type={this.props?.isMultiSelect ? "select-multiple" : undefined}
/>
</DropdownSelect>
);
}
renderItem = (option: DropdownOption, itemProps: IItemRendererProps) => {
if (!itemProps.modifiers.matchesPredicate) {
return null;
}
const isSelected: boolean = this.isOptionSelected(option);
return (
<MenuItem
active={isSelected}
className="single-select"
key={option.value}
onClick={itemProps.handleClick}
text={option.label}
/>
);
};
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 (
<Dropdown
boundary="window"
dontUsePortal={false}
dropdownMaxHeight="250px"
errorMsg={props.props?.errorText}
helperText={props.props?.info}
isMultiSelect={props?.props?.isMultiSelect}
onSelect={props.input?.onChange}
optionWidth="50vh"
options={props.options}
placeholder={props.props?.placeholderText}
selected={selectedOption}
showLabelOnly
width={props?.props?.width ? props?.props?.width : "50vh"}
/>
);
}
export interface DropDownControlProps extends ControlProps {
options: DropdownOption[];
placeholderText: string;
propertyValue: string;
subtitle?: string;
isMultiSelect?: boolean;
isDisabled?: boolean;
isSearchable?: boolean;
}

View File

@ -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 (
<div style={customStyle}>
<FormLabel>
{label} {isRequired && "*"}
</FormLabel>
<StyledDynamicTextField
dataTreePath={dataTreePath}
name={name}

View File

@ -3,7 +3,6 @@ import { formValueSelector, change } from "redux-form";
import { connect } from "react-redux";
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 {
EditorSize,
@ -64,7 +63,6 @@ class DynamicTextControl extends BaseControl<
actionName,
configProperty,
evaluationSubstitutionType,
label,
placeholderText,
responseType,
} = this.props;
@ -81,7 +79,6 @@ class DynamicTextControl extends BaseControl<
return (
<Wrapper>
<FormLabel>{label}</FormLabel>
{showTemplate ? (
<TemplateMenu
createTemplate={(templateString) => {

View File

@ -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 (
<>
<FormLabel>{label}</FormLabel>
<FieldArray
component={NestedComponents}
name={configProperty}
props={{ formName, schema }}
rerenderOnEveryChange={false}
/>
</>
<FieldArray
component={NestedComponents}
name={configProperty}
props={{ formName, schema }}
rerenderOnEveryChange={false}
/>
);
}

View File

@ -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<Props, FieldFileInputState> {
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 (
<>
<div
className={replayHighlightClass}
style={{ flexDirection: "row", display: "flex", width: "50vh" }}
>
<StyledDiv>{value.name}</StyledDiv>
<StyledDiv>{props?.input?.value?.name}</StyledDiv>
<SelectButton
buttonStyle="PRIMARY"
buttonVariant={ButtonVariantTypes.SECONDARY}
onClick={() => {
this.openModal();
setIsOpen(true);
}}
text={"Select"}
/>
</div>
);
}
{isOpen ? (
<DialogComponent
canOutsideClickClose
isOpen={isOpen}
maxHeight={"540px"}
setModalClose={() => setIsOpen(false)}
>
<FilePickerWrapper>
<FilePickerV2
delayedUpload
fileType={FileType.ANY}
fileUploader={FileUploader}
onFileRemoved={onRemoveFile}
/>
</FilePickerWrapper>
</DialogComponent>
) : null}
</>
);
}
class FilePickerControl extends BaseControl<FilePickerControlProps> {
constructor(props: FilePickerControlProps) {
super(props);
@ -136,16 +145,8 @@ class FilePickerControl extends BaseControl<FilePickerControlProps> {
}
render() {
const { configProperty, isRequired, label } = this.props;
return (
<>
<FormLabel>
{label} {isRequired && "*"}
</FormLabel>
<Field component={FieldFileInput} name={configProperty} />
</>
);
const { configProperty } = this.props;
return <Field component={RenderFilePicker} name={configProperty} />;
}
getControlType(): ControlType {

View File

@ -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<FixedKeyInputControlProps> {
render() {
const {
configProperty,
dataType,
fixedKey,
isRequired,
label,
placeholderText,
} = this.props;
const { configProperty, dataType, fixedKey, placeholderText } = this.props;
return (
<Wrapper>
<FormLabel>
{label} {isRequired && "*"}
</FormLabel>
<TextField
format={(value) => {
// Get the value property

View File

@ -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 (
<div data-cy={name} style={{ width: "50vh" }}>
<FormLabel>
{label} {isRequired && "*"}{" "}
{encrypted && (
<>
<FormIcons.LOCK_ICON height={12} keepColors width={12} />
<StyledInfo>Encrypted</StyledInfo>
</>
)}
{subtitle && (
<>
<br />
<StyledInfo>{subtitle}</StyledInfo>
</>
)}
</FormLabel>
<TextField
<Field
component={renderComponent}
datatype={dataType}
disabled={disabled || false}
name={name}
placeholder={placeholder}
showError
type={dataType}
{...props}
asyncControl
/>
</div>
);
}
function renderComponent(
props: {
placeholder: string;
dataType?: InputType;
disabled?: boolean;
} & {
meta: Partial<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
) {
return (
<TextInput
dataType={props.dataType}
disabled={props.disabled || false}
name={props.input?.name}
onChange={props.input.onChange}
placeholder={props.placeholder}
value={props.input.value}
{...props.input}
width="100%"
/>
);
}
class InputTextControl extends BaseControl<InputControlProps> {
render() {
const {
configProperty,
dataType,
disabled,
encrypted,
isValid,
label,
placeholderText,
@ -86,7 +90,7 @@ class InputTextControl extends BaseControl<InputControlProps> {
<InputText
dataType={this.getType(dataType)}
disabled={disabled}
encrypted={this.props.encrypted}
encrypted={encrypted}
isValid={isValid}
label={label}
name={configProperty}

View File

@ -1,15 +1,28 @@
import React, { useEffect, useCallback, JSXElementConstructor } from "react";
import { FieldArray, WrappedFieldArrayProps } from "redux-form";
import React, { useEffect, useCallback } from "react";
import {
Field,
FieldArray,
WrappedFieldArrayProps,
WrappedFieldMetaProps,
WrappedFieldInputProps,
} from "redux-form";
import styled from "styled-components";
import { Icon } from "@blueprintjs/core";
import { FormIcons } from "icons/FormIcons";
import BaseControl, { ControlProps } from "./BaseControl";
import TextField from "components/editorComponents/form/fields/TextField";
import BaseControl, { ControlProps, ControlData } from "./BaseControl";
import { ControlType } from "constants/PropertyControlConstants";
import DynamicTextField from "components/editorComponents/form/fields/DynamicTextField";
import FormLabel from "components/editorComponents/FormLabel";
import HelperTooltip from "components/editorComponents/HelperTooltip";
import { Colors } from "constants/Colors";
import TextInput, { TextInputProps } from "components/ads/TextInput";
export interface KeyValueArrayControlProps extends ControlProps {
name: string;
label: string;
maxLen?: number;
description?: string;
actionConfig?: any;
extraData?: ControlData[];
isRequired?: boolean;
}
const FormRowWithLabel = styled.div`
display: flex;
@ -20,28 +33,32 @@ const FormRowWithLabel = styled.div`
}
`;
const StyledTextField = styled(TextField)`
const StyledTextInput = styled(TextInput)`
min-width: 66px;
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
margin: 0px;
}
`;
function KeyValueRow(props: KeyValueArrayProps & WrappedFieldArrayProps) {
function KeyValueRow(
props: KeyValueArrayControlProps & WrappedFieldArrayProps,
) {
const { extraData = [] } = props;
const keyName = getFieldName(extraData[0].configProperty);
const valueName = getFieldName(extraData[1].configProperty);
const valueDataType = getType(extraData[1].dataType);
const keyName = getFieldName(extraData[0]?.configProperty);
const valueName = getFieldName(extraData[1]?.configProperty);
const keyFieldProps = extraData[0];
let isRequired: boolean | undefined;
useEffect(() => {
// 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<string, any> = {};
if (
props.actionConfig &&
props.actionConfig[index].description &&
props.rightIcon
) {
otherProps.rightIcon = (
<HelperTooltip
description={props.actionConfig[index].description}
rightIcon={
props.actionConfig[index].description && props.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 (
<FormRowWithLabel key={index} style={{ marginTop: 16 }}>
<FormRowWithLabel
key={index}
style={{ marginTop: index > 0 ? "16px" : "0px" }}
>
<div
data-replay-id={btoa(`${field}.${keyName[1]}`)}
data-replay-id={btoa(keyTextFieldName)}
style={{ width: "50vh" }}
>
<FormLabel>
{extraData && extraData[0].label} {isRequired && "*"}
</FormLabel>
<TextField
name={`${field}.${keyName[1]}`}
placeholder={(extraData && extraData[0].placeholderText) || ""}
showError
validate={keyFieldValidate}
<Field
component={renderTextInput}
name={keyTextFieldName}
props={{
dataType: getType(extraData[0]?.dataType),
defaultValue: props.initialValue,
keyFieldValidate,
placeholder: props.extraData
? props.extraData[1]?.placeholderText
: "",
isRequired: extraData[0]?.isRequired,
name: keyTextFieldName,
}}
/>
</div>
{!props.actionConfig && (
<div style={{ marginLeft: 16 }}>
<FormLabel>
{extraData && extraData[1].label} {isRequired && "*"}
</FormLabel>
<div style={{ display: "flex", flexDirection: "row" }}>
<div
data-replay-id={btoa(`${field}.${valueName[1]}`)}
style={{ marginRight: 14, width: 72 }}
>
<StyledTextField
name={`${field}.${valueName[1]}`}
placeholder={
(extraData && extraData[1].placeholderText) || ""
}
type={valueDataType}
/>
</div>
{index === props.fields.length - 1 ? (
<div style={{ marginLeft: "16px", width: "50vh" }}>
<div
data-replay-id={valueTextFieldName}
style={{ display: "flex", flexDirection: "row" }}
>
<Field
component={renderTextInput}
name={valueTextFieldName}
props={{
dataType: getType(extraData[1]?.dataType),
defaultValue: props.initialValue,
placeholder: props.extraData
? props.extraData[1]?.placeholderText
: "",
name: valueTextFieldName,
isRequired: extraData[1]?.isRequired,
}}
/>
{showAddIcon(index) ? (
<Icon
className="t--add-field"
color={Colors["CADET_BLUE"]}
icon="plus"
iconSize={20}
onClick={() => props.fields.push({ key: "", value: "" })}
style={{ alignSelf: "center" }}
onClick={() => {
props.fields.push({ key: "", value: "" });
}}
style={{ marginLeft: "16px", alignSelf: "center" }}
/>
) : (
<FormIcons.DELETE_ICON
className="t--delete-field"
color={Colors["CADET_BLUE"]}
height={20}
onClick={() => 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}
/>
)}
</FormRowWithLabel>
@ -165,7 +190,7 @@ function KeyValueRow(props: KeyValueArrayProps & WrappedFieldArrayProps) {
) : null;
}
class KeyValueFieldArray extends BaseControl<KeyValueArrayProps> {
class KeyValueArrayControl extends BaseControl<KeyValueArrayControlProps> {
render() {
const name = getFieldName(this.props.configProperty);
@ -174,7 +199,7 @@ class KeyValueFieldArray extends BaseControl<KeyValueArrayProps> {
component={KeyValueRow}
rerenderOnEveryChange={false}
{...this.props}
name={name[0]}
name={name ? name[0] : ""}
/>
);
}
@ -184,8 +209,8 @@ class KeyValueFieldArray extends BaseControl<KeyValueArrayProps> {
}
}
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<WrappedFieldMetaProps>;
input: Partial<WrappedFieldInputProps>;
},
): JSX.Element {
return (
<StyledTextInput
dataType={props.dataType}
defaultValue={props.defaultValue}
errorMsg={props.errorMsg}
helperText={props.helperText}
name={props.input?.name}
onChange={props.input.onChange}
placeholder={props.placeholder}
validator={props.keyFieldValidate}
value={props.input.value}
width="100%"
/>
);
}
export default KeyValueFieldArray;
export default KeyValueArrayControl;

View File

@ -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) {
<div>
{typeof props.fields.getAll() === "object" && (
<div>
<FormLabel>
{props.label} {props.isRequired && "*"}
</FormLabel>
{props.fields.map((field: any, index: number) => (
<FormRowWithLabel key={index} style={{ marginTop: index ? 13 : 0 }}>
<div
data-replay-id={btoa(`${field}.key`)}
style={{ width: "50vh" }}
>
{/* <FormLabel></FormLabel> */}
<TextField name={`${field}.key`} placeholder="Key" />
</div>
<div style={{ marginLeft: 16 }}>
{/* <FormLabel></FormLabel> */}
<div style={{ display: "flex", flexDirection: "row" }}>
<div
data-replay-id={btoa(`${field}.value`)}
@ -83,17 +97,7 @@ function KeyValueRow(props: Props & WrappedFieldArrayProps) {
);
}
type Props = {
name: string;
label: string;
rightIcon?: JSXElementConstructor<{ height: number; width: number }>;
description?: string;
actionConfig?: any;
extraData?: ControlData[];
isRequired?: boolean;
};
class KeyValueFieldInput extends BaseControl<KeyValueInputProps> {
class KeyValueFieldInputControl extends BaseControl<KeyValueInputControlProps> {
render() {
return (
<FieldArray
@ -110,12 +114,4 @@ class KeyValueFieldInput extends BaseControl<KeyValueInputProps> {
}
}
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;

View File

@ -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<Props, any> {
export class SwitchField extends React.Component<SwitchFieldProps, any> {
get value() {
const { input } = this.props;
if (typeof input.value !== "string") return !!input.value;
@ -44,21 +47,18 @@ export class SwitchField extends React.Component<Props, any> {
}
render() {
const { info, input, isRequired, label } = this.props;
return (
<div>
<SwitchWrapped data-cy={this.props.input.name}>
<StyledFormLabel>
{label} {isRequired && "*"}
</StyledFormLabel>
<StyledSwitch
checked={this.value}
large
onChange={(value) => input.onChange(value)}
<StyledToggle
className="switch-control"
name={this.props.input.name}
onToggle={(value: any) => {
this.props.input.onChange(value);
}}
value={this.value}
/>
</SwitchWrapped>
{info && <Info>{info}</Info>}
</div>
);
}

View File

@ -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 (
<PrimaryBox style={{ width: `${props.maxWidth}vh`, marginTop }}>
<PrimaryBox style={{ marginTop }}>
<SecondaryBox>
{/* Component to render the joining operator between multiple conditions */}
<FormControl
@ -302,8 +301,6 @@ function ConditionBlock(props: any) {
},
],
});
// eslint-disable-next-line no-console
console.log("Ayush new field", props.fields.getAll());
}}
>
{/*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 (
<>
<FormLabel>{label}</FormLabel>
<FieldArray
component={ConditionBlock}
key={`${configProperty}.children`}
name={`${configProperty}.children`}
props={{
configProperty,
maxWidth,
formName,
logicalTypes,
comparisonTypes,
nestedLevels,
currentNestingLevel: 0,
}}
rerenderOnEveryChange={false}
/>
</>
<FieldArray
component={ConditionBlock}
key={`${configProperty}.children`}
name={`${configProperty}.children`}
props={{
configProperty,
maxWidth,
formName,
logicalTypes,
comparisonTypes,
nestedLevels,
currentNestingLevel: 0,
}}
rerenderOnEveryChange={false}
/>
);
}

View File

@ -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: {
<InfoIcon />
</IconWrapper>
),
HELP_ICON: (props: IconProps) => (
<IconWrapper {...props}>
<HelpIcon />
</IconWrapper>
),
HOME_ICON: (props: IconProps) => (
<IconWrapper {...props}>
<Icon color={props.color} icon={IconNames.HOME} iconSize={props.height} />

View File

@ -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<Props> {
componentDidMount() {
const search = new URLSearchParams(this.props.location.search);
@ -380,36 +368,35 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
<Callout fill key={i} text={msg} variant={Variant.warning} />
))}
<FormInputContainer data-replay-id={btoa("url")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="url"
isRequired
label="URL"
placeholderText="https://example.com"
/>
{this.renderInputTextControlViaFormControl(
"url",
"URL",
"https://example.com",
"TEXT",
false,
true,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("headers")}>
<KeyValueInputControl
{...COMMON_INPUT_PROPS}
configProperty="headers"
label="Headers"
/>
{this.renderKeyValueControlViaFormControl(
"headers",
"Headers",
"",
false,
)}
</FormInputContainer>
<FormInputContainer>
<KeyValueInputControl
{...COMMON_INPUT_PROPS}
configProperty="queryParameters"
label="Query Parameters"
/>
<FormInputContainer data-replay-id={btoa("queryParameters")}>
{this.renderKeyValueControlViaFormControl(
"queryParameters",
"Query Parameters",
"",
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("isSendSessionEnabled")}>
<DropDownControl
{...COMMON_INPUT_PROPS}
configProperty="isSendSessionEnabled"
isRequired
label="Send Appsmith signature header"
options={[
{this.renderDropdownControlViaFormControl(
"isSendSessionEnabled",
[
{
label: "Yes",
value: true,
@ -418,28 +405,29 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
label: "No",
value: false,
},
]}
placeholderText=""
propertyValue=""
subtitle="Header key: X-APPSMITH-SIGNATURE"
/>
],
"Send Appsmith signature header",
"",
true,
"Header key: X-APPSMITH-SIGNATURE",
)}
</FormInputContainer>
{formData.isSendSessionEnabled && (
<FormInputContainer data-replay-id={btoa("sessionSignatureKey")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="sessionSignatureKey"
label="Session Details Signature Key"
placeholderText=""
/>
{this.renderInputTextControlViaFormControl(
"sessionSignatureKey",
"Session Details Signature Key",
"",
"TEXT",
false,
false,
)}
</FormInputContainer>
)}
<FormInputContainer data-replay-id={btoa("authType")}>
<DropDownControl
{...COMMON_INPUT_PROPS}
configProperty="authType"
label="Authentication Type"
options={[
{this.renderDropdownControlViaFormControl(
"authType",
[
{
label: "None",
value: AuthType.NONE,
@ -460,10 +448,12 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
label: "Bearer Token",
value: AuthType.bearerToken,
},
]}
placeholderText=""
propertyValue=""
/>
],
"Authentication Type",
"",
false,
"",
)}
</FormInputContainer>
{this.renderAuthFields()}
</>
@ -497,28 +487,29 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
return (
<>
<FormInputContainer data-replay-id={btoa("authentication.label")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.label"
label="Key"
placeholderText="api_key"
/>
{this.renderInputTextControlViaFormControl(
"authentication.label",
"Key",
"api_key",
"TEXT",
false,
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.value")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.value"
encrypted
label="Value"
placeholderText="value"
/>
<FormInputContainer>
{this.renderInputTextControlViaFormControl(
"authentication.value",
"Value",
"value",
"TEXT",
true,
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.addTo")}>
<DropDownControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.addTo"
label="Add To"
options={[
<FormInputContainer>
{this.renderDropdownControlViaFormControl(
"authentication.addTo",
[
{
label: "Query Params",
value: ApiKeyAuthType.QueryParams,
@ -527,21 +518,25 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
label: "Header",
value: ApiKeyAuthType.Header,
},
]}
placeholderText=""
propertyValue=""
/>
],
"Add To",
"",
false,
"",
)}
</FormInputContainer>
{_.get(authentication, "addTo") == "header" && (
<FormInputContainer
data-replay-id={btoa("authentication.headerPrefix")}
>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.headerPrefix"
label="Header Prefix"
placeholderText="eg: Bearer "
/>
{this.renderInputTextControlViaFormControl(
"authentication.headerPrefix",
"Header Prefix",
"eg: Bearer ",
"TEXT",
false,
false,
)}
</FormInputContainer>
)}
</>
@ -551,13 +546,14 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
renderBearerToken = () => {
return (
<FormInputContainer data-replay-id={btoa("authentication.bearerToken")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.bearerToken"
encrypted
label="Bearer Token"
placeholderText="Bearer Token"
/>
{this.renderInputTextControlViaFormControl(
"authentication.bearerToken",
"Bearer Token",
"Bearer Token",
"TEXT",
true,
false,
)}
</FormInputContainer>
);
};
@ -566,22 +562,24 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
return (
<>
<FormInputContainer data-replay-id={btoa("authentication.username")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.username"
label="Username"
placeholderText="Username"
/>
{this.renderInputTextControlViaFormControl(
"authentication.username",
"Username",
"Username",
"TEXT",
false,
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.password")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.password"
dataType="PASSWORD"
encrypted
label="Password"
placeholderText="Password"
/>
{this.renderInputTextControlViaFormControl(
"authentication.password",
"Password",
"Password",
"PASSWORD",
true,
false,
)}
</FormInputContainer>
</>
);
@ -603,11 +601,9 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
return (
<>
<FormInputContainer data-replay-id={btoa("authentication.grantType")}>
<DropDownControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.grantType"
label="Grant Type"
options={[
{this.renderDropdownControlViaFormControl(
"authentication.grantType",
[
{
label: "Client Credentials",
value: GrantType.ClientCredentials,
@ -616,10 +612,12 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
label: "Authorization Code",
value: GrantType.AuthorizationCode,
},
]}
placeholderText=""
propertyValue=""
/>
],
"Grant Type",
"",
false,
"",
)}
</FormInputContainer>
{content}
</>
@ -633,11 +631,9 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
<FormInputContainer
data-replay-id={btoa("authentication.isTokenHeader")}
>
<DropDownControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.isTokenHeader"
label="Add Access Token To"
options={[
{this.renderDropdownControlViaFormControl(
"authentication.isTokenHeader",
[
{
label: "Request Header",
value: true,
@ -646,58 +642,70 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
label: "Request URL",
value: false,
},
]}
/>
],
"Add Access Token To",
"",
false,
"",
)}
</FormInputContainer>
{_.get(formData.authentication, "isTokenHeader") && (
<FormInputContainer
data-replay-id={btoa("authentication.headerPrefix")}
>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.headerPrefix"
label="Header Prefix"
placeholderText="eg: Bearer "
/>
{this.renderInputTextControlViaFormControl(
"authentication.headerPrefix",
"Header Prefix",
"eg: Bearer ",
"TEXT",
false,
false,
)}
</FormInputContainer>
)}
<FormInputContainer
data-replay-id={btoa("authentication.accessTokenUrl")}
>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.accessTokenUrl"
label="Access Token URL"
placeholderText="https://example.com/login/oauth/access_token"
/>
{this.renderInputTextControlViaFormControl(
"authentication.accessTokenUrl",
"Access Token URL",
"https://example.com/login/oauth/access_token",
"TEXT",
false,
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.clientId")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.clientId"
label="Client ID"
placeholderText="Client ID"
/>
{this.renderInputTextControlViaFormControl(
"authentication.clientId",
"Client ID",
"Client ID",
"TEXT",
false,
false,
)}
</FormInputContainer>
<FormInputContainer
data-replay-id={btoa("authentication.clientSecret")}
>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.clientSecret"
dataType="PASSWORD"
encrypted
label="Client Secret"
placeholderText="Client Secret"
/>
{this.renderInputTextControlViaFormControl(
"authentication.clientSecret",
"Client Secret",
"Client Secret",
"PASSWORD",
true,
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.scopeString")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.scopeString"
label="Scope(s)"
placeholderText="e.g. read, write"
/>
{this.renderInputTextControlViaFormControl(
"authentication.scopeString",
"Scope(s)",
"e.g. read, write",
"TEXT",
false,
false,
)}
</FormInputContainer>
</>
);
@ -706,28 +714,25 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
renderOauth2CommonAdvanced = () => {
return (
<>
<FormInputContainer>
<KeyValueInputControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.customTokenParameters"
label="Custom Token Parameters"
/>
</FormInputContainer>
<FormInputContainer>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.audience"
label="Audience"
placeholderText="https://example.com/oauth/audience"
/>
<FormInputContainer data-replay-id={btoa("authentication.audience")}>
{this.renderInputTextControlViaFormControl(
"authentication.audience",
"Audience",
"https://example.com/oauth/audience",
"TEXT",
false,
false,
)}
</FormInputContainer>
<FormInputContainer data-replay-id={btoa("authentication.resource")}>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.resource"
label="Resource"
placeholderText="https://example.com/oauth/resource"
/>
{this.renderInputTextControlViaFormControl(
"authentication.resource",
"Resource",
"https://example.com/oauth/resource",
"TEXT",
false,
false,
)}
</FormInputContainer>
</>
);
@ -757,12 +762,14 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
<FormInputContainer
data-replay-id={btoa("authentication.authorizationUrl")}
>
<InputTextControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.authorizationUrl"
label="Authorization URL"
placeholderText="https://example.com/login/oauth/authorize"
/>
{this.renderInputTextControlViaFormControl(
"authentication.authorizationUrl",
"Authorization URL",
"https://example.com/login/oauth/authorize",
"TEXT",
false,
false,
)}
</FormInputContainer>
<FormInputContainer>
<div style={{ width: "50vh" }}>
@ -779,20 +786,19 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
<FormInputContainer
data-replay-id={btoa("authentication.customAuthenticationParameters")}
>
<KeyValueInputControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.customAuthenticationParameters"
label="Custom Authentication Parameters"
/>
{this.renderKeyValueControlViaFormControl(
"authentication.customAuthenticationParameters",
"Custom Authentication Parameters",
"",
false,
)}
</FormInputContainer>
<FormInputContainer
data-replay-id={btoa("authentication.isAuthorizationHeader")}
>
<DropDownControl
{...COMMON_INPUT_PROPS}
configProperty="authentication.isAuthorizationHeader"
label="Client Authentication"
options={[
{this.renderDropdownControlViaFormControl(
"authentication.isAuthorizationHeader",
[
{
label: "Send as Basic Auth header",
value: true,
@ -801,8 +807,12 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
label: "Send client credentials in body",
value: false,
},
]}
/>
],
"Client Authentication",
"",
false,
"",
)}
</FormInputContainer>
{!_.get(formData.authentication, "isAuthorizationHeader", true) &&
this.renderOauth2CommonAdvanced()}
@ -824,6 +834,93 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
</>
);
};
// 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 (
<FormControl
config={{
id: "",
isValid: false,
isRequired: isRequired,
controlType: "INPUT_TEXT",
dataType: dataType,
configProperty: configProperty,
encrypted: encrypted,
label: label,
conditionals: "",
placeholderText: placeholderText,
formName: DATASOURCE_REST_API_FORM,
}}
formName={DATASOURCE_REST_API_FORM}
multipleConfig={[]}
/>
);
}
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 (
<FormControl
config={config}
formName={DATASOURCE_REST_API_FORM}
multipleConfig={[]}
/>
);
}
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 (
<FormControl
config={config}
formName={DATASOURCE_REST_API_FORM}
multipleConfig={[]}
/>
);
}
}
const mapStateToProps = (state: AppState, props: any) => {

View File

@ -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 (
<div
className={`t--form-control-${props.config.controlType}`}
data-replay-id={btoa(props.config.configProperty)}
<FormConfig
config={props.config}
formName={props.formName}
multipleConfig={props?.multipleConfig}
>
{FormControlFactory.createControl(
props.config,
props.formName,
props?.multipleConfig,
)}
<div className={`t--form-control-${props.config.controlType}`}>
{FormControlFactory.createControl(
props.config,
props.formName,
props?.multipleConfig,
)}
</div>
</FormConfig>
);
}
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 = (
<div style={{ display: "flex" }}>
{props.multipleConfig?.map((config) => {
return renderFormConfigTop({ config });
})}
</div>
);
bottom = props.multipleConfig?.map((config) => {
return renderFormConfigBottom({ config });
});
return (
<>
{top}
{props.children}
{bottom}
</>
);
}
return (
<div>
<div
style={{
// TODO: replace condition with props.config.dataType === "TOGGLE"
// label and form element is rendered side by side for CHECKBOX and SWITCH
display:
props.config.controlType === "SWITCH" ||
props.config.controlType === "CHECKBOX"
? "flex"
: "block",
}}
>
{props.config.controlType === "CHECKBOX" ? (
<>
{props.children}
{renderFormConfigTop({ config: props.config })}
</>
) : (
<>
{renderFormConfigTop({ config: props.config })}
{props.children}
</>
)}
</div>
{renderFormConfigBottom({ config: props.config })}
</div>
);
}
export default FormControl;
function renderFormConfigTop(props: { config: ControlProps }) {
const {
displayType,
encrypted,
isRequired,
label,
subtitle,
tooltipText = "",
url,
urlText,
} = { ...props.config };
return (
<React.Fragment key={props.config.label}>
<FormLabel config={props.config}>
<p className="label-icon-wrapper">
{label} {isRequired && "*"}{" "}
{encrypted && (
<>
<FormIcons.LOCK_ICON height={12} keepColors width={12} />
<FormSubtitleText config={props.config}>
Encrypted
</FormSubtitleText>
</>
)}
{tooltipText && (
<Tooltip content={tooltipText} hoverOpenDelay={1000}>
<FormIcons.HELP_ICON height={16} width={16} />
</Tooltip>
)}
</p>
{subtitle && (
<FormInfoText config={props.config}>{subtitle}</FormInfoText>
)}
</FormLabel>
{urlText && (
<FormInputAnchor href={url} target="_blank">
{urlText}
</FormInputAnchor>
)}
{displayType && (
<FormInputSwitchToJsonButton type="button">
{displayType === "JSON" ? "SWITCH TO GUI" : "SWITCH TO JSON EDITOR"}
</FormInputSwitchToJsonButton>
)}
</React.Fragment>
);
}
function renderFormConfigBottom(props: { config: ControlProps }) {
const { errorText, info, showError } = { ...props.config };
return (
<>
{info && <FormInputHelperText>{info}</FormInputHelperText>}
{showError && <FormInputErrorText>{errorText}</FormInputErrorText>}
</>
);
}

View File

@ -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}
/>
</CreateIconWrapper>
) : pluginImages[dropdownOption.data.pluginId] ? (
) : pluginImages[(dropdownOption as DropdownOption).data.pluginId] ? (
<ImageWrapper>
<DatasourceImage
alt=""
className="dataSourceImage"
src={pluginImages[dropdownOption.data.pluginId]}
src={
pluginImages[(dropdownOption as DropdownOption).data.pluginId]
}
/>
</ImageWrapper>
) : null}

View File

@ -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}

View File

@ -23,7 +23,7 @@ function SelectedValueNode(props: DefaultDropDownValueNodeProps) {
const { selected } = props;
return (
<SelectedValueNodeContainer>
<span className="label">{selected.label}</span>
<span className="label">{(selected as DropdownOption).label}</span>
<ChevronDown />
</SelectedValueNodeContainer>
);

View File

@ -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 <FixedKeyInputControl {...controlProps} />;
},
});
@ -66,17 +66,23 @@ class FormControlRegistry {
},
});
FormControlFactory.registerControlBuilder("KEYVALUE_ARRAY", {
buildPropertyControl(controlProps: KeyValueArrayProps): JSX.Element {
buildPropertyControl(
controlProps: KeyValueArrayControlProps,
): JSX.Element {
return <KeyValueArrayControl {...controlProps} />;
},
});
FormControlFactory.registerControlBuilder("FILE_PICKER", {
buildPropertyControl(controlProps: FilePickerControlProps): JSX.Element {
//used by redshift datasource
return <FilePickerControl {...controlProps} />;
},
});
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 <KeyValueInputControl {...controlProps} />;
},
});
@ -94,12 +100,13 @@ class FormControlRegistry {
});
FormControlFactory.registerControlBuilder("CHECKBOX", {
buildPropertyControl(controlProps: CheckboxControlProps): JSX.Element {
//used in API datasource form only
return <CheckboxControl {...controlProps} />;
},
});
FormControlFactory.registerControlBuilder("NUMBER_INPUT", {
buildPropertyControl(controlProps: InputControlProps): JSX.Element {
return <InputNumberControl {...controlProps} />;
return <InputTextControl {...controlProps} />;
},
});
FormControlFactory.registerControlBuilder("ARRAY_FIELD", {