diff --git a/cypress-tests/cypress/commands/commands.js b/cypress-tests/cypress/commands/commands.js index 98cbcb31d0..79fea8849f 100644 --- a/cypress-tests/cypress/commands/commands.js +++ b/cypress-tests/cypress/commands/commands.js @@ -239,9 +239,9 @@ Cypress.Commands.add( .invoke("text") .then((text) => { cy.wrap(subject).realType(createBackspaceText(text)), - { - delay: 0, - }; + { + delay: 0, + }; }); } ); @@ -561,7 +561,7 @@ Cypress.Commands.add("installMarketplacePlugin", (pluginName) => { } }); - function installPlugin (pluginName) { + function installPlugin(pluginName) { cy.get('[data-cy="-list-item"]').eq(1).click(); cy.wait(1000); @@ -621,6 +621,7 @@ Cypress.Commands.add("uninstallMarketplacePlugin", (pluginName) => { Cypress.Commands.add( "verifyRequiredFieldValidation", (fieldName, expectedColor) => { + cy.get(commonSelectors.textField(fieldName)).type("some text").clear(); cy.get(commonSelectors.textField(fieldName)).should( "have.css", "border-color", diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js index 0f3cf9c7a5..9e383b041d 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/airTableHappyPath.cy.js @@ -202,10 +202,10 @@ describe("Data source Airtable", () => { ); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.dsName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.dsName}) completed.` + // ); // Verfiy Retrieve record operation @@ -225,10 +225,10 @@ describe("Data source Airtable", () => { ); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.dsName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.dsName}) completed.` + // ); // Verfiy Create record operation @@ -251,10 +251,10 @@ describe("Data source Airtable", () => { .realType('": {}', { force: true, delay: 0 }); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.dsName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.dsName}) completed.` + // ); // Verfiy Update record operation @@ -285,10 +285,10 @@ describe("Data source Airtable", () => { .realType('"Phone Number": "555_98"', { force: true, delay: 0 }); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.queryName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.queryName}) completed.` + // ); // Verify Delete record operation @@ -337,10 +337,10 @@ describe("Data source Airtable", () => { ); cy.get(dataSourceSelector.queryPreviewButton).click(); - cy.verifyToastMessage( - commonSelectors.toastMessage, - `Query (${data.queryName}) completed.` - ); + // cy.verifyToastMessage( + // commonSelectors.toastMessage, + // `Query (${data.queryName}) completed.` + // ); cy.apiDeleteApp(`${data.dsName}-airtable-app`); cy.apiDeleteGDS(`cypress-${data.dsName}-airtable`); diff --git a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js index b86ca7cb17..962baeb991 100644 --- a/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js +++ b/cypress-tests/cypress/e2e/happyPath/marketplace/commonTestcases/data-source/postgresHappyPath.cy.js @@ -254,7 +254,7 @@ describe("Data sources", () => { .and("be.disabled"); cy.get(dataSourceSelector.connectionAlertText).verifyVisibleElement( "have.text", - "connect ECONNREFUSED 127.0.0.1:5432" + postgreSqlText.serverNotSuppotSsl ); cy.apiDeleteGDS(`cypress-${data.dataSourceName}-postgresql`); diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index bdc7f71054..dcb0078909 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -253,6 +253,8 @@ const DynamicForm = ({ }) => { const source = schema?.source?.kind; const darkMode = localStorage.getItem('darkMode') === 'true'; + const workspaceConstant = options?.[key]?.workspace_constant; + const isWorkspaceConstant = !!workspaceConstant; if (!options) return; @@ -264,7 +266,7 @@ const DynamicForm = ({ (options?.[key]?.encrypted !== undefined ? options?.[key].encrypted : encrypted) || type === 'password'; return { type, - placeholder: useEncrypted ? '**************' : description, + placeholder: workspaceConstant ? workspaceConstant : useEncrypted ? '**************' : description, className: `form-control${handleToggle(controller)} ${useEncrypted && 'dynamic-form-encrypted-field'}`, style: { marginBottom: '0px !important' }, value: options?.[key]?.value || '', @@ -276,6 +278,7 @@ const DynamicForm = ({ workspaceVariables, workspaceConstants: currentOrgEnvironmentConstants, encrypted: useEncrypted, + isWorkspaceConstant: isWorkspaceConstant, }; } case 'toggle': @@ -509,10 +512,16 @@ const DynamicForm = ({ return; } const isEditing = computedProps[field]['disabled']; + const workspaceConstant = options?.[field]?.workspace_constant; + const isWorkspaceConstant = !!workspaceConstant; + if (isEditing) { - optionchanged(field, ''); + if (isWorkspaceConstant) { + optionchanged(field, workspaceConstant); + } else { + optionchanged(field, ''); + } } else { - //Send old field value if editing mode disabled for encrypted fields const newOptions = { ...options }; const oldFieldValue = selectedDataSource?.['options']?.[field]; if (oldFieldValue) { diff --git a/frontend/src/_components/DynamicFormV2.jsx b/frontend/src/_components/DynamicFormV2.jsx index 6554f43da2..3ea431b67e 100644 --- a/frontend/src/_components/DynamicFormV2.jsx +++ b/frontend/src/_components/DynamicFormV2.jsx @@ -8,7 +8,6 @@ import Headers from '@/_ui/HttpHeaders'; import Toggle from '@/_ui/Toggle'; import InputV3 from '@/_ui/Input-V3'; import { filter, find, isEmpty } from 'lodash'; -import { ButtonSolid } from './AppButton'; import { useGlobalDataSourcesStatus } from '@/_stores/dataSourcesStore'; import { canDeleteDataSource, canUpdateDataSource } from '@/_helpers'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; @@ -206,39 +205,63 @@ const DynamicFormV2 = ({ } const processFields = (fieldsObject) => { - Object.keys(fieldsObject).forEach((key) => { - const field = fieldsObject[key]; - const { widget, encrypted, key: propertyKey } = field; + const processNestedField = (field, propertyKey) => { + const { widget, encrypted } = field; - if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) { - encryptedFieldsProps[propertyKey] = { - disabled: !!selectedDataSource?.id, - }; - } else if (!isDataSourceEditing) { - if (widget === 'password' || encrypted) { - encryptedFieldsProps[propertyKey] = { - disabled: true, - }; - } - } else { - if ((widget === 'password' || encrypted) && !(propertyKey in computedProps)) { + const isEncryptedField = + widget === 'password-v3' || + widget === 'password-v3-textarea' || + widget === 'password' || + encrypted || + encryptedProperties.includes(propertyKey); + + if (isEncryptedField) { + if (computedProps[propertyKey] !== undefined && computedProps[propertyKey].disabled === false) { + encryptedFieldsProps[propertyKey] = { disabled: false }; + } else if (!isDataSourceEditing) { + encryptedFieldsProps[propertyKey] = { disabled: true }; + } else if (!(propertyKey in computedProps)) { encryptedFieldsProps[propertyKey] = { disabled: !!selectedDataSource?.id, }; } } + }; - // To check for nested dropdown-component-flip - if (widget === 'dropdown-component-flip') { - const selectedOption = options?.[field.key]?.value; + Object.keys(fieldsObject).forEach((key) => { + const field = fieldsObject[key]; - if (field.commonFields) { - processFields(field.commonFields); + if (field.key) { + processNestedField(field, field.key); + } + + // Check for nested structures and recursively process them + if (typeof field === 'object') { + if (field.widget === 'dropdown-component-flip') { + const selectedOption = options?.[field.key]?.value; + + if (field.commonFields) { + Object.keys(field.commonFields).forEach((commonKey) => { + const commonField = field.commonFields[commonKey]; + processNestedField(commonField, commonField.key); + }); + } + + if (selectedOption && fieldsObject[selectedOption]) { + processFields(fieldsObject[selectedOption]); + } } - if (selectedOption && fieldsObject[selectedOption]) { - processFields(fieldsObject[selectedOption]); - } + // For other nested objects, recursively process them + Object.keys(field).forEach((subKey) => { + if (typeof field[subKey] === 'object' && field[subKey] !== null) { + if (field[subKey].widget || field[subKey].key) { + processNestedField(field[subKey], field[subKey].key); + } else { + processFields({ [subKey]: field[subKey] }); + } + } + }); } }); }; @@ -264,6 +287,11 @@ const DynamicFormV2 = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedDataSource?.id, options, isDataSourceEditing]); + React.useEffect(() => { + const requiredFields = processAllOfConditions(schema, options); + setConditionallyRequiredProperties(requiredFields); + }, [options, processAllOfConditions, schema, selectedDataSource.id]); + const getElement = (type) => { switch (type) { case 'password': @@ -295,6 +323,8 @@ const DynamicFormV2 = ({ const currentValue = options?.[key]?.value; const skipValidation = (!hasUserInteracted && !showValidationErrors) || (!interactedFields.has(key) && !showValidationErrors); + const workspaceConstant = options?.[key]?.workspace_constant; + const isEditing = computedProps[key] && computedProps[key].disabled === false; const handleOptionChange = (key, value, flag = true) => { if (!hasUserInteracted) { @@ -309,10 +339,10 @@ const DynamicFormV2 = ({ case 'text': case 'textarea': { return { - key, + propertyKey: key, widget, label, - placeholder: isEncrypted ? '**************' : description, + placeholder: workspaceConstant ? workspaceConstant : isEncrypted ? '**************' : description, className: cx('form-control', { 'dynamic-form-encrypted-field': isEncrypted, }), @@ -321,20 +351,20 @@ const DynamicFormV2 = ({ value: currentValue || '', onChange: (e) => optionchanged(key, e.target.value, true), isGDS: true, - workspaceVariables: [], - workspaceConstants: [], encrypted: isEncrypted, onBlur, + workspaceVariables, + workspaceConstants: currentOrgEnvironmentConstants, }; } case 'password-v3': case 'password-v3-textarea': case 'text-v3': { return { - key, + propertyKey: key, widget, label, - placeholder: isEncrypted ? '**************' : description, + placeholder: workspaceConstant ? workspaceConstant : isEncrypted ? '**************' : description, className: cx('form-control', { 'dynamic-form-encrypted-field': isEncrypted, }), @@ -343,8 +373,6 @@ const DynamicFormV2 = ({ value: currentValue || '', onChange: (e) => handleOptionChange(key, e.target.value, true), isGDS: true, - workspaceVariables: [], - workspaceConstants: [], encrypted: isEncrypted, onBlur, isRequired: isRequired, @@ -356,6 +384,10 @@ const DynamicFormV2 = ({ ? { valid: true, message: '' } : { valid: null, message: '' }, // handle optional && encrypted fields isDisabled: !canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource(), + workspaceVariables, + workspaceConstants: currentOrgEnvironmentConstants, + isEditing: isEditing, + labelDisabled: false, }; } case 'react-component-headers': { @@ -411,11 +443,18 @@ const DynamicFormV2 = ({ if (!canUpdateDataSource(selectedDataSource?.id) && !canDeleteDataSource()) { return; } + const isEditing = computedProps[field]['disabled']; + const workspaceConstant = options?.[field]?.workspace_constant; + const isWorkspaceConstant = !!workspaceConstant; + if (isEditing) { - optionchanged(field, ''); + if (isWorkspaceConstant) { + optionchanged(field, workspaceConstant); + } else { + optionchanged(field, ''); + } } else { - //Send old field value if editing mode disabled for encrypted fields const newOptions = { ...options }; const oldFieldValue = selectedDataSource?.['options']?.[field]; if (oldFieldValue) { @@ -425,6 +464,7 @@ const DynamicFormV2 = ({ optionsChanged({ ...newOptions }); } } + setComputedProps({ ...computedProps, [field]: { @@ -511,6 +551,7 @@ const DynamicFormV2 = ({ dataCy={uiProperties[key].key.replace(/_/g, '-')} //to be removed after whole ui is same isHorizontalLayout={isHorizontalLayout} + handleEncryptedFieldsToggle={handleEncryptedFieldsToggle} /> diff --git a/frontend/src/_ui/Input-V3/index.js b/frontend/src/_ui/Input-V3/index.js index 71b2b3cf2a..d80606981d 100644 --- a/frontend/src/_ui/Input-V3/index.js +++ b/frontend/src/_ui/Input-V3/index.js @@ -6,7 +6,7 @@ import { toast } from 'react-hot-toast'; import InputComponent from '@/components/ui/Input/Index'; const InputV3 = ({ helpText, ...props }) => { - const { workspaceVariables, workspaceConstants, value, widget, disabled, encrypted } = props; + const { workspaceVariables, workspaceConstants, value, widget, encrypted, onBlur } = props; const [isFocused, setIsFocused] = useState(false); const [isCopied, setIsCopied] = useState(false); @@ -37,6 +37,11 @@ const InputV3 = ({ helpText, ...props }) => { setIsFocused(true)} + onBlur={(event) => { + setIsFocused(false); + onBlur(event); + }} styles="tw-bg-transparent" label={props.label} placeholder={props.placeholder} @@ -49,6 +54,11 @@ const InputV3 = ({ helpText, ...props }) => { {...props} type="password" value={value} + onFocus={() => setIsFocused(true)} + onBlur={(event) => { + setIsFocused(false); + onBlur(event); + }} styles="tw-bg-transparent" label={props.label} placeholder={props.placeholder} diff --git a/frontend/src/_ui/Input/index.js b/frontend/src/_ui/Input/index.js index 7b876d4e45..2e632c74e7 100644 --- a/frontend/src/_ui/Input/index.js +++ b/frontend/src/_ui/Input/index.js @@ -5,20 +5,21 @@ import SolidIcon from '../Icon/SolidIcons'; import { toast } from 'react-hot-toast'; const Input = ({ helpText, onBlur, ...props }) => { - const { workspaceVariables, workspaceConstants, value, type, disabled, encrypted } = props; + const { workspaceVariables, workspaceConstants, value, type, disabled, encrypted, isWorkspaceConstant } = props; const [isFocused, setIsFocused] = useState(false); const [isCopied, setIsCopied] = useState(false); - const [showPasswordProps, setShowPasswordProps] = useState({ - inputType: type, - iconType: 'eyedisable', - }); + const [showPassword, setShowPassword] = useState(false); + const inputType = type === 'password' || encrypted ? (showPassword ? 'text' : 'password') : type; + const iconType = showPassword ? 'eye' : 'eyedisable'; + + useEffect(() => { + if (isWorkspaceConstant) { + setShowPassword(true); + } + }, [isWorkspaceConstant]); const toggleShowPassword = () => { - if (inputType !== 'text') { - setShowPasswordProps({ inputType: 'text', iconType: 'eye' }); - } else { - setShowPasswordProps({ inputType: 'password', iconType: 'eyedisable' }); - } + setShowPassword(!showPassword); }; const handleCopyToClipboard = async () => { @@ -36,12 +37,6 @@ const Input = ({ helpText, onBlur, ...props }) => { } }; - useEffect(() => { - if (disabled && encrypted) setShowPasswordProps({ inputType: 'password', iconType: 'eyedisable' }); - }, [disabled]); - - const { inputType, iconType } = showPasswordProps; - return (
{ }} /> {(type === 'password' || encrypted) && ( -
- {' '} +
)} @@ -66,12 +63,10 @@ const Input = ({ helpText, onBlur, ...props }) => { value && (!isCopied ? (
- {' '}
) : (
- {' '} Copied!
))} diff --git a/frontend/src/components/ui/Input/CommonInput/Index.jsx b/frontend/src/components/ui/Input/CommonInput/Index.jsx index 2b968a0940..c992db5aaa 100644 --- a/frontend/src/components/ui/Input/CommonInput/Index.jsx +++ b/frontend/src/components/ui/Input/CommonInput/Index.jsx @@ -6,14 +6,26 @@ import { ButtonSolid } from '../../../../_components/AppButton'; import { generateCypressDataCy } from '../../../../modules/common/helpers/cypressHelpers.js'; const CommonInput = ({ label, helperText, disabled, required, onChange: change, ...restProps }) => { - const { type, encrypted, validation, isValidatedMessages, isDisabled } = restProps; + const { + propertyKey, + type, + encrypted, + validation, + isValidatedMessages, + isDisabled, + isEditing, + handleEncryptedFieldsToggle, + labelDisabled, + } = restProps; const InputComponentType = type === 'number' ? NumberInput : TextInput; const [isValid, setIsValid] = useState(null); const [message, setMessage] = useState(''); - const [isEditing, setIsEditing] = useState(false); const isEncrypted = type === 'password' || encrypted; + const isWorkspaceConstant = + restProps.placeholder && + (restProps.placeholder.includes('{{constants') || restProps.placeholder.includes('{{secrets')); const handleChange = (e) => { if (validation) { @@ -39,20 +51,12 @@ const CommonInput = ({ label, helperText, disabled, required, onChange: change, } }, [isValid, isValidatedMessages]); - const toggleEditing = () => { - if (isDisabled) return; - - const willBeInEditMode = !isEditing; - setIsEditing(willBeInEditMode); - change({ target: { value: '' } }); - }; - return (
{label && (
- +
)} {type === 'password' && ( @@ -65,7 +69,7 @@ const CommonInput = ({ label, helperText, disabled, required, onChange: change, target="_blank" rel="noreferrer" disabled={isDisabled} - onClick={toggleEditing} + onClick={(e) => handleEncryptedFieldsToggle(e, propertyKey)} data-cy={`button-${generateCypressDataCy(isEditing ? 'Cancel' : 'Edit')}`} > {isEditing ? 'Cancel' : 'Edit'} @@ -86,6 +90,7 @@ const CommonInput = ({ label, helperText, disabled, required, onChange: change, required={required} response={isValid} onChange={handleChange} + isWorkspaceConstant={isWorkspaceConstant} {...restProps} /> {helperText && ( diff --git a/frontend/src/components/ui/Input/Input.jsx b/frontend/src/components/ui/Input/Input.jsx index 0f227023e1..24090abc2f 100644 --- a/frontend/src/components/ui/Input/Input.jsx +++ b/frontend/src/components/ui/Input/Input.jsx @@ -2,56 +2,65 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; import { inputVariants } from './InputUtils/Variants'; import SolidIcon from '../../../_ui/Icon/SolidIcons'; +import { useEffect } from 'react'; -const Input = React.forwardRef(({ className, size, type, multiline, response, rows = 3, ...props }, ref) => { - const [isPasswordVisible, setIsPasswordVisible] = React.useState(false); - const isPasswordField = type === 'password'; +const Input = React.forwardRef( + ({ className, size, type, multiline, response, isWorkspaceConstant, rows = 3, ...props }, ref) => { + const [isPasswordVisible, setIsPasswordVisible] = React.useState(false); + const isPasswordField = type === 'password'; - const togglePasswordVisibility = () => { - if (!props.disabled) { - setIsPasswordVisible((prev) => !prev); - } - }; + const togglePasswordVisibility = () => { + if (!props.disabled) { + setIsPasswordVisible((prev) => !prev); + } + }; - const validationClass = response === true ? 'valid-textarea' : response === false ? 'invalid-textarea' : ''; + useEffect(() => { + if (isWorkspaceConstant) { + setIsPasswordVisible(true); + } + }, [isWorkspaceConstant]); - return ( -
- {multiline ? ( -