diff --git a/.env.example b/.env.example index a98011bb3d..6dfdfab187 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,9 @@ APPSMITH_MAIL_SMTP_TLS_ENABLED= #APPSMITH_SENTRY_ENVIRONMENT= # Configure cloud services -# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com" \ No newline at end of file +# APPSMITH_CLOUD_SERVICES_BASE_URL="https://release-cs.appsmith.com" + +# Google Recaptcha Config +APPSMITH_RECAPTCHA_SITE_KEY= +APPSMITH_RECAPTCHA_SECRET_KEY= +APPSMITH_RECAPTCHA_ENABLED= \ No newline at end of file diff --git a/app.json b/app.json index d6bf73c26b..1a1fb89536 100644 --- a/app.json +++ b/app.json @@ -98,6 +98,21 @@ "value": "", "required": false }, + "APPSMITH_RECAPTCHA_SITE_KEY": { + "description" : "Google reCAPTCHA v3 site key, it is required if you wish to enable protection against spam/abusive users. Read more at: https://developers.google.com/recaptcha/docs/v3", + "value": "", + "required": false + }, + "APPSMITH_RECAPTCHA_SECRET_KEY": { + "description" : "Google reCAPTCHA v3 verification secret key, it is required if you wish to enable spam protection in your backend server.", + "value": "", + "required": false + }, + "APPSMITH_RECAPTCHA_ENABLED": { + "description" : "Boolean config to enable or disable Google reCAPTCHA v3 verification feature. If set to true, both site key and secret key should be provided.", + "value": "", + "required": false + }, "APPSMITH_DISABLE_TELEMETRY": { "description" : "We want to be transparent and request that you share anonymous usage data with us. This data is purely statistical in nature and helps us understand your needs & provide better support to your self-hosted instance. You can read more about what information is collected in our documentation https://docs.appsmith.com/v/v1.2.1/setup/telemetry", "value": "false" diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js index 06b63c3340..ae64c14c37 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/ExplorerTests/Entity_Explorer_DragAndDropWidget_spec.js @@ -1,5 +1,6 @@ const testdata = require("../../../../fixtures/testdata.json"); const apiwidget = require("../../../../locators/apiWidgetslocator.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); const explorer = require("../../../../locators/explorerlocators.json"); const commonlocators = require("../../../../locators/commonlocators.json"); const formWidgetsPage = require("../../../../locators/FormWidgets.json"); @@ -30,10 +31,13 @@ describe("Entity explorer Drag and Drop widgets testcases", function() { /** * @param{Text} Random Colour */ - cy.testCodeMirror(this.data.colour); + cy.get(widgetsPage.backgroundcolorPicker) + .first() + .click({ force: true }); + cy.xpath(widgetsPage.greenColor).click(); cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); /** * @param{toggleButton Css} Assert to be checked */ diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js index 50bef3f6c9..70cef20567 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/FormWidget_spec.js @@ -3,6 +3,7 @@ const formWidgetsPage = require("../../../../locators/FormWidgets.json"); const publish = require("../../../../locators/publishWidgetspage.json"); const dsl = require("../../../../fixtures/formdsl.json"); const pages = require("../../../../locators/Pages.json"); +const widgetsPage = require("../../../../locators/Widgets.json"); describe("Form Widget Functionality", function() { before(() => { @@ -23,10 +24,13 @@ describe("Form Widget Functionality", function() { /** * @param{Text} Random Colour */ - cy.testCodeMirror(this.data.colour); + cy.get(widgetsPage.backgroundcolorPicker) + .first() + .click({ force: true }); + cy.xpath(widgetsPage.greenColor).click(); cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); /** * @param{toggleButton Css} Assert to be checked */ @@ -40,7 +44,7 @@ describe("Form Widget Functionality", function() { it("Form Widget Functionality To Verify The Colour", function() { cy.get(formWidgetsPage.formD) .should("have.css", "background-color") - .and("eq", this.data.rgbValue); + .and("eq", "rgb(3, 179, 101)"); }); it("Form Widget Functionality To Unchecked Visible Widget", function() { cy.get(publish.backToEditor).click(); diff --git a/app/client/docker/templates/nginx-app.conf.template b/app/client/docker/templates/nginx-app.conf.template index 4b7c5fdf4e..6b8ed93474 100644 --- a/app/client/docker/templates/nginx-app.conf.template +++ b/app/client/docker/templates/nginx-app.conf.template @@ -48,6 +48,7 @@ server { sub_filter __APPSMITH_MAIL_ENABLED__ '${APPSMITH_MAIL_ENABLED}'; sub_filter __APPSMITH_DISABLE_TELEMETRY__ '${APPSMITH_DISABLE_TELEMETRY}'; sub_filter __APPSMITH_CLOUD_SERVICES_BASE_URL__ '${APPSMITH_CLOUD_SERVICES_BASE_URL}'; + sub_filter __APPSMITH_RECAPTCHA_SITE_KEY__ '${APPSMITH_RECAPTCHA_SITE_KEY}'; } location /f { diff --git a/app/client/netlify.toml b/app/client/netlify.toml index 3bb6abd265..9c900dc92d 100644 --- a/app/client/netlify.toml +++ b/app/client/netlify.toml @@ -8,6 +8,7 @@ REACT_APP_ALGOLIA_SEARCH_INDEX_NAME = "test_appsmith" REACT_APP_CLIENT_LOG_LEVEL = "debug" REACT_APP_GOOGLE_MAPS_API_KEY = "AIzaSyBOQFulljufGt3VDhBAwNjZN09KEFufVyg" + REACT_APP_GOOGLE_RECAPTCHA_SITE_KEY = "" REACT_APP_TNC_PP = "true" REACT_APP_CLOUD_HOSTING = "true" REACT_APP_INTERCOM_APP_ID = "y10e7138" diff --git a/app/client/package.json b/app/client/package.json index 430c1e6f98..afb8a5cc0e 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -152,7 +152,6 @@ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=dev.appsmith.com craco start", - "start-m1": "BROWSER=none EXTEND_ESLINT=true REACT_APP_ENVIRONMENT=DEVELOPMENT HOST=0.0.0.0 craco start", "build": "./build.sh", "build-local": "craco --max-old-space-size=4096 build --config craco.build.config.js", "build-staging": "REACT_APP_ENVIRONMENT=STAGING craco --max-old-space-size=4096 build --config craco.build.config.js", @@ -164,7 +163,8 @@ "test:unit": "$(npm bin)/jest -b --colors --no-cache --coverage --collectCoverage=true --coverageDirectory='../../' --coverageReporters='json-summary'", "test:jest": "$(npm bin)/jest --watch", "storybook": "start-storybook -p 9009 -s public", - "build-storybook": "build-storybook -s public" + "build-storybook": "build-storybook -s public", + "postinstall": "patch-package" }, "resolution": { "jest": "24.8.0" @@ -237,6 +237,8 @@ "mochawesome": "^5.0.0", "mochawesome-report-generator": "^4.1.0", "msw": "^0.28.0", + "patch-package": "^6.4.7", + "postinstall-postinstall": "^2.1.0", "raw-loader": "^4.0.2", "react-docgen-typescript-loader": "^3.6.0", "react-is": "^16.12.0", diff --git a/app/client/patches/@blueprintjs+core+3.36.0.patch b/app/client/patches/@blueprintjs+core+3.36.0.patch new file mode 100644 index 0000000000..3342cbfefc --- /dev/null +++ b/app/client/patches/@blueprintjs+core+3.36.0.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js +index 84f03fa..5e5488a 100644 +--- a/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js ++++ b/node_modules/@blueprintjs/core/lib/esm/components/editable-text/editableText.js +@@ -188,7 +188,16 @@ var EditableText = /** @class */ (function (_super) { + if (this.state.isEditing && !prevState.isEditing) { + (_b = (_a = this.props).onEdit) === null || _b === void 0 ? void 0 : _b.call(_a, this.state.value); + } +- this.updateInputDimensions(); ++ // updateInputDimensions is an expensive method. Call it only when the props ++ // it depends on change ++ if (this.state.value !== prevState.value || ++ this.props.alwaysRenderInput !== prevProps.alwaysRenderInput || ++ this.props.maxLines !== prevProps.maxLines || ++ this.props.minLines !== prevProps.minLines || ++ this.props.minWidth !== prevProps.minWidth || ++ this.props.multiline !== prevProps.multiline) { ++ this.updateInputDimensions(); ++ } + }; + EditableText.prototype.renderInput = function (value) { + var _a = this.props, disabled = _a.disabled, maxLength = _a.maxLength, multiline = _a.multiline, type = _a.type, placeholder = _a.placeholder; diff --git a/app/client/public/index.html b/app/client/public/index.html index 8079bbba08..df6675bb58 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -204,7 +204,8 @@ intercomAppID: APP_ID, mailEnabled: parseConfig("__APPSMITH_MAIL_ENABLED__"), disableTelemetry: DISABLE_TELEMETRY === "" || DISABLE_TELEMETRY, - cloudServicesBaseUrl: parseConfig("__APPSMITH_CLOUD_SERVICES_BASE_URL__") || "https://cs.appsmith.com" + cloudServicesBaseUrl: parseConfig("__APPSMITH_CLOUD_SERVICES_BASE_URL__") || "https://cs.appsmith.com", + googleRecaptchaSiteKey: parseConfig("__APPSMITH_RECAPTCHA_SITE_KEY__"), }; diff --git a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx index 44b5338312..daa1459448 100644 --- a/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx +++ b/app/client/src/components/designSystems/blueprint/ButtonComponent.tsx @@ -180,8 +180,25 @@ function RecaptchaComponent( }); props.onClick && props.onClick(event); } + + // Check if a string is a valid JSON string + const checkValidJson = (inputString: string): boolean => { + try { + JSON.parse(inputString); + return true; + } catch (err) { + return false; + } + }; + + let validGoogleRecaptchaKey = props.googleRecaptchaKey; + + if (validGoogleRecaptchaKey && checkValidJson(validGoogleRecaptchaKey)) { + validGoogleRecaptchaKey = undefined; + } + const status = useScript( - `https://www.google.com/recaptcha/api.js?render=${props.googleRecaptchaKey}`, + `https://www.google.com/recaptcha/api.js?render=${validGoogleRecaptchaKey}`, ); return (
void; + onModalClose?: () => void; children: ReactNode; width?: number; className?: string; @@ -76,6 +77,14 @@ export function ModalComponent(props: ModalComponentProps) { const modalContentRef: RefObject = useRef( null, ); + useEffect(() => { + return () => { + // handle modal close events when this component unmounts + // will be called in all cases :- + // escape key press, click out side, close click from other btn widget + if (props.onModalClose) props.onModalClose(); + }; + }, []); useEffect(() => { if (!props.scrollContents) { modalContentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); diff --git a/app/client/src/components/editorComponents/EditableText.tsx b/app/client/src/components/editorComponents/EditableText.tsx index b8c2983a02..baa81e42c0 100644 --- a/app/client/src/components/editorComponents/EditableText.tsx +++ b/app/client/src/components/editorComponents/EditableText.tsx @@ -92,10 +92,25 @@ const TextContainer = styled.div<{ isValid: boolean; minimal: boolean }>` `; export function EditableText(props: EditableTextProps) { - const [isEditing, setIsEditing] = useState(!!props.isEditingDefault); - const [value, setStateValue] = useState(props.defaultValue); + const { + beforeUnmount, + className, + defaultValue, + editInteractionKind, + forceDefault, + hideEditIcon, + isEditingDefault, + isInvalid, + minimal, + onBlur, + onTextChanged, + placeholder, + updating, + valueTransform, + } = props; + const [isEditing, setIsEditing] = useState(!!isEditingDefault); + const [value, setStateValue] = useState(defaultValue); const inputValRef = useRef(""); - const { beforeUnmount } = props; const setValue = useCallback((value) => { inputValRef.current = value; @@ -103,16 +118,16 @@ export function EditableText(props: EditableTextProps) { }, []); useEffect(() => { - setValue(props.defaultValue); - }, [props.defaultValue]); + setValue(defaultValue); + }, [defaultValue]); useEffect(() => { - setIsEditing(!!props.isEditingDefault); - }, [props.defaultValue, props.isEditingDefault]); + setIsEditing(!!isEditingDefault); + }, [defaultValue, isEditingDefault]); useEffect(() => { - if (props.forceDefault === true) setValue(props.defaultValue); - }, [props.forceDefault, props.defaultValue]); + if (forceDefault === true) setValue(defaultValue); + }, [forceDefault, defaultValue]); // at times onTextChange is not fired // for example when the modal is closed on clicking the overlay @@ -128,58 +143,63 @@ export function EditableText(props: EditableTextProps) { e.preventDefault(); e.stopPropagation(); }; - const onChange = (_value: string) => { - props.onBlur && props.onBlur(); - const isInvalid = props.isInvalid ? props.isInvalid(_value) : false; - if (!isInvalid) { - props.onTextChanged(_value); - setIsEditing(false); - } else { - Toaster.show({ - text: "Invalid name", - variant: Variant.danger, - }); - } - }; + const onChange = useCallback( + (_value: string) => { + onBlur && onBlur(); + const _isInvalid = isInvalid ? isInvalid(_value) : false; + if (!_isInvalid) { + onTextChanged(_value); + setIsEditing(false); + } else { + Toaster.show({ + text: "Invalid name", + variant: Variant.danger, + }); + } + }, + [isInvalid], + ); - const onInputchange = (_value: string) => { - let finalVal: string = _value; - if (props.valueTransform) { - finalVal = props.valueTransform(_value); - } - setValue(finalVal); - }; + const onInputchange = useCallback( + (_value: string) => { + let finalVal: string = _value; + if (valueTransform) { + finalVal = valueTransform(_value); + } + setValue(finalVal); + }, + [valueTransform], + ); - const errorMessage = props.isInvalid && props.isInvalid(value); + const errorMessage = isInvalid && isInvalid(value); const error = errorMessage ? errorMessage : undefined; return ( - + - {!props.minimal && - !props.hideEditIcon && - !props.updating && - !isEditing && } + {!minimal && !hideEditIcon && !updating && !isEditing && ( + + )} diff --git a/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx new file mode 100644 index 0000000000..ad5dfb33b4 --- /dev/null +++ b/app/client/src/components/editorComponents/GlobalSearch/Footer.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import styled from "styled-components"; + +const Wrapper = styled.div` + span { + color: white; + font-variant: all-small-caps; + font-size: ${(props) => props.theme.fontSizes[2]}px; + margin-right: ${(props) => props.theme.spaces[1]}px; + } + div { + margin-right: ${(props) => props.theme.spaces[7]}px; + font-size: ${(props) => props.theme.fontSizes[2]}px; + text-align: center; + } + color: ${(props) => props.theme.colors.globalSearch.searchItemText}; + padding: ${(props) => props.theme.spaces[1]}px + ${(props) => props.theme.spaces[6]}px; + display: flex; + flex-direction: row; +`; + +const FOOTER_INFO = [ + { + action: "\u2191\u2193", + description: "Select", + }, + { + action: "ENTER", + description: "Open", + }, +]; + +function Footer() { + return ( + + {FOOTER_INFO.map((info) => { + return ( +
+ {info.action} + {info.description} +
+ ); + })} +
+ ); +} + +export default Footer; diff --git a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx index f1af2aa4fc..84766c4720 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/ResultsNotFound.tsx @@ -4,6 +4,7 @@ import NoSearchDataImage from "assets/images/no_search_data.png"; import { NO_SEARCH_DATA_TEXT } from "constants/messages"; import { getTypographyByKey } from "constants/DefaultTheme"; import { ReactComponent as DiscordIcon } from "assets/icons/help/discord.svg"; +import AnalyticsUtil from "utils/AnalyticsUtil"; const Container = styled.div` display: flex; @@ -49,6 +50,7 @@ function ResultsNotFound() { className="discord-link" onClick={() => { window.open("https://discord.gg/rBTTVJp", "_blank"); + AnalyticsUtil.logEvent("DISCORD_LINK_CLICK"); }} > diff --git a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx index 5da83a7c9e..42a7cb03e3 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/SearchResults.tsx @@ -25,6 +25,7 @@ import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers"; import { AppState } from "reducers"; import { keyBy, noop } from "lodash"; import { getPageList } from "selectors/editorSelectors"; +import { PluginType } from "entities/Action"; const DocumentIcon = HelpIcons.DOCUMENT; @@ -158,10 +159,13 @@ function ActionItem(props: { return state.entities.plugins.list; }); const pluginGroups = useMemo(() => keyBy(plugins, "id"), [plugins]); - const icon = getActionConfig(pluginType)?.getIcon( - item.config, - pluginGroups[item.config.datasource.pluginId], - ); + const icon = + pluginType === PluginType.API + ? getActionConfig(pluginType)?.icon + : getActionConfig(pluginType)?.getIcon( + item.config, + pluginGroups[item.config.datasource.pluginId], + ); let title = getItemTitle(item); const pageName = usePageName(config.pageId); @@ -293,9 +297,7 @@ function SearchItemComponent(props: ItemProps) { itemType !== SEARCH_ITEM_TYPES.placeholder ) { setActiveItemIndex(index); - if (itemType !== SEARCH_ITEM_TYPES.document) { - searchContext?.handleItemLinkClick(item, "SEARCH_ITEM"); - } + searchContext?.handleItemLinkClick(item, "SEARCH_ITEM"); } }} ref={itemRef} diff --git a/app/client/src/components/editorComponents/GlobalSearch/index.tsx b/app/client/src/components/editorComponents/GlobalSearch/index.tsx index 113936eb1b..07577ddb08 100644 --- a/app/client/src/components/editorComponents/GlobalSearch/index.tsx +++ b/app/client/src/components/editorComponents/GlobalSearch/index.tsx @@ -46,6 +46,7 @@ import { keyBy, noop } from "lodash"; import EntitiesIcon from "assets/icons/ads/entities.svg"; import DocsIcon from "assets/icons/ads/docs.svg"; import RecentIcon from "assets/icons/ads/recent.svg"; +import Footer from "./Footer"; const StyledContainer = styled.div` width: 750px; @@ -204,7 +205,7 @@ function GlobalSearch() { ); }, [pages, query]); - const recentsSectionTitle = getSectionTitle("Recents", RecentIcon); + const recentsSectionTitle = getSectionTitle("Recent Entities", RecentIcon); const docsSectionTitle = getSectionTitle("Documentation Links", DocsIcon); const entitiesSectionTitle = getSectionTitle("Entities", EntitiesIcon); @@ -397,6 +398,7 @@ function GlobalSearch() { )}
+ {!query &&