From 451742951d316dc15420f0297fb253479f82eeaf Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Wed, 8 Oct 2025 23:53:15 +0300 Subject: [PATCH 1/8] feat(UI-1600): reddit connection --- src/assets/image/icons/connections/Reddit.svg | 8 ++ src/assets/image/icons/connections/index.ts | 3 + .../connections/integrations/index.ts | 2 + .../connections/integrations/reddit/add.tsx | 114 ++++++++++++++++ .../connections/integrations/reddit/edit.tsx | 128 ++++++++++++++++++ .../connections/integrations/reddit/index.ts | 2 + .../integrationVariablesMapping.constants.ts | 7 + .../integrationsDataKeys.constants.ts | 1 + src/enums/components/connection.enum.ts | 7 + src/validations/connection.schema.ts | 8 ++ src/validations/index.ts | 1 + 11 files changed, 281 insertions(+) create mode 100644 src/assets/image/icons/connections/Reddit.svg create mode 100644 src/components/organisms/connections/integrations/reddit/add.tsx create mode 100644 src/components/organisms/connections/integrations/reddit/edit.tsx create mode 100644 src/components/organisms/connections/integrations/reddit/index.ts diff --git a/src/assets/image/icons/connections/Reddit.svg b/src/assets/image/icons/connections/Reddit.svg new file mode 100644 index 0000000000..c651d7c981 --- /dev/null +++ b/src/assets/image/icons/connections/Reddit.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/image/icons/connections/index.ts b/src/assets/image/icons/connections/index.ts index f9d00e8402..bf3944d733 100644 --- a/src/assets/image/icons/connections/index.ts +++ b/src/assets/image/icons/connections/index.ts @@ -41,3 +41,6 @@ export { default as MicrosoftTeamsIcon } from "@assets/image/icons/connections/M export { default as KubernetesIcon } from "@assets/image/icons/connections/Kubernetes.svg?react"; // Taken from: https://icons.getbootstrap.com/icons/anthropic/ export { default as AnthropicIcon } from "@assets/image/icons/connections/Anthropic.svg?react"; +// Taken from: https://www.iconpacks.net/free-icon/reddit-circle-logo-16620.html +// Terms: https://www.iconpacks.net/terms/ +export { default as RedditIcon } from "@assets/image/icons/connections/Reddit.svg?react"; diff --git a/src/components/organisms/connections/integrations/index.ts b/src/components/organisms/connections/integrations/index.ts index dababec302..dacacabab6 100644 --- a/src/components/organisms/connections/integrations/index.ts +++ b/src/components/organisms/connections/integrations/index.ts @@ -66,3 +66,5 @@ export { KubernetesIntegrationAddForm, KubernetesIntegrationEditForm, } from "@components/organisms/connections/integrations/kubernetes"; +export { RedditIntegrationAddForm } from "@components/organisms/connections/integrations/reddit"; +export { RedditIntegrationEditForm } from "@components/organisms/connections/integrations/reddit"; diff --git a/src/components/organisms/connections/integrations/reddit/add.tsx b/src/components/organisms/connections/integrations/reddit/add.tsx new file mode 100644 index 0000000000..41b35cd39d --- /dev/null +++ b/src/components/organisms/connections/integrations/reddit/add.tsx @@ -0,0 +1,114 @@ +import React, { useEffect } from "react"; + +import { useTranslation } from "react-i18next"; + +import { ConnectionAuthType } from "@src/enums"; +import { Integrations } from "@src/enums/components"; +import { useConnectionForm } from "@src/hooks"; +import { redditPrivateAuthIntegrationSchema } from "@validations"; + +import { Button, ErrorMessage, Input, Spinner } from "@components/atoms"; + +import { FloppyDiskIcon } from "@assets/image/icons"; + +export const RedditIntegrationAddForm = ({ + connectionId, + triggerParentFormSubmit, +}: { + connectionId?: string; + triggerParentFormSubmit: () => void; +}) => { + const { t } = useTranslation("integrations"); + + const { createConnection, errors, handleSubmit, isLoading, register } = useConnectionForm( + redditPrivateAuthIntegrationSchema, + "create" + ); + + useEffect(() => { + if (connectionId) { + createConnection(connectionId, ConnectionAuthType.OauthPrivate, Integrations.reddit); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectionId]); + + return ( +
+
+ + + {errors.client_id?.message as string} +
+ +
+ + + {errors.client_secret?.message as string} +
+ +
+ + + {errors.user_agent?.message as string} +
+ +
+ + + {errors.username?.message as string} +
+ +
+ + + {errors.password?.message as string} +
+ + +
+ ); +}; diff --git a/src/components/organisms/connections/integrations/reddit/edit.tsx b/src/components/organisms/connections/integrations/reddit/edit.tsx new file mode 100644 index 0000000000..f9e693ba46 --- /dev/null +++ b/src/components/organisms/connections/integrations/reddit/edit.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from "react"; + +import { useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +import { integrationVariablesMapping } from "@src/constants"; +import { useConnectionForm } from "@src/hooks"; +import { setFormValues } from "@src/utilities"; +import { redditPrivateAuthIntegrationSchema } from "@validations"; + +import { Button, ErrorMessage, Input, SecretInput, Spinner } from "@components/atoms"; + +import { FloppyDiskIcon } from "@assets/image/icons"; + +export const RedditIntegrationEditForm = () => { + const { t } = useTranslation("integrations"); + const [lockState, setLockState] = useState({ + client_secret: true, + password: true, + }); + const { connectionVariables, control, errors, handleSubmit, isLoading, onSubmitEdit, register, setValue } = + useConnectionForm(redditPrivateAuthIntegrationSchema, "edit"); + + const clientId = useWatch({ control, name: "client_id" }); + const clientSecret = useWatch({ control, name: "client_secret" }); + const userAgent = useWatch({ control, name: "user_agent" }); + const username = useWatch({ control, name: "username" }); + const password = useWatch({ control, name: "password" }); + + useEffect(() => { + setFormValues(connectionVariables, integrationVariablesMapping.reddit, setValue); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [connectionVariables]); + + return ( +
+
+ + {errors.client_id?.message as string} +
+ +
+ setValue("client_secret", newValue)} + handleLockAction={(newLockState: boolean) => + setLockState((prevState) => ({ ...prevState, client_secret: newLockState })) + } + isError={!!errors.client_secret} + isLocked={lockState.client_secret} + isRequired + label={t("reddit.placeholders.clientSecret")} + value={clientSecret} + /> + + {errors.client_secret?.message as string} +
+ +
+ + + {errors.user_agent?.message as string} +
+ +
+ + + {errors.username?.message as string} +
+ +
+ setValue("password", newValue)} + handleLockAction={(newLockState: boolean) => + setLockState((prevState) => ({ ...prevState, password: newLockState })) + } + isError={!!errors.password} + isLocked={lockState.password} + label={t("reddit.placeholders.password")} + value={password} + /> + + {errors.password?.message as string} +
+ + +
+ ); +}; diff --git a/src/components/organisms/connections/integrations/reddit/index.ts b/src/components/organisms/connections/integrations/reddit/index.ts new file mode 100644 index 0000000000..6e6db1c5fc --- /dev/null +++ b/src/components/organisms/connections/integrations/reddit/index.ts @@ -0,0 +1,2 @@ +export { RedditIntegrationAddForm } from "@components/organisms/connections/integrations/reddit/add"; +export { RedditIntegrationEditForm } from "@components/organisms/connections/integrations/reddit/edit"; diff --git a/src/constants/connections/integrationVariablesMapping.constants.ts b/src/constants/connections/integrationVariablesMapping.constants.ts index acda8a517a..c323c3536b 100644 --- a/src/constants/connections/integrationVariablesMapping.constants.ts +++ b/src/constants/connections/integrationVariablesMapping.constants.ts @@ -108,4 +108,11 @@ export const integrationVariablesMapping = { [Integrations.kubernetes]: { config_file: "config_file", }, + [Integrations.reddit]: { + client_id: "client_id", + client_secret: "client_secret", + user_agent: "user_agent", + username: "username", + password: "password", + }, }; diff --git a/src/constants/connections/integrationsDataKeys.constants.ts b/src/constants/connections/integrationsDataKeys.constants.ts index 5a6c1db7fa..053893ca3a 100644 --- a/src/constants/connections/integrationsDataKeys.constants.ts +++ b/src/constants/connections/integrationsDataKeys.constants.ts @@ -8,4 +8,5 @@ export const integrationDataKeys = { zoom: ["account_id", "client_id", "client_secret", "secret_token"], salesforce: ["client_id", "client_secret"], microsoft_teams: ["client_id", "client_secret", "tenant_id", "auth_scopes"], + reddit: ["client_id", "client_secret", "user_agent", "username", "password"], }; diff --git a/src/enums/components/connection.enum.ts b/src/enums/components/connection.enum.ts index e1a3effe20..7e5fbdeeab 100644 --- a/src/enums/components/connection.enum.ts +++ b/src/enums/components/connection.enum.ts @@ -34,6 +34,7 @@ import { MicrosoftTeamsIcon, KubernetesIcon, AnthropicIcon, + RedditIcon, } from "@assets/image/icons/connections"; export enum ConnectionStatus { @@ -71,6 +72,7 @@ export enum Integrations { // eslint-disable-next-line @typescript-eslint/naming-convention microsoft_teams = "microsoft_teams", kubernetes = "kubernetes", + reddit = "reddit", } export const defaultGoogleConnectionName = "google"; @@ -245,6 +247,11 @@ export const IntegrationsMap: Record = { label: "Kubernetes", value: Integrations.kubernetes, }, + reddit: { + icon: RedditIcon, + label: "Reddit", + value: Integrations.reddit, + }, }; const shouldHideIntegration: Partial> = { diff --git a/src/validations/connection.schema.ts b/src/validations/connection.schema.ts index 501a1e0481..626339d403 100644 --- a/src/validations/connection.schema.ts +++ b/src/validations/connection.schema.ts @@ -173,6 +173,14 @@ export const microsoftTeamsIntegrationSchema = z.object({ tenant_id: z.string().min(1, "Tenant ID is required"), }); +export const redditPrivateAuthIntegrationSchema = z.object({ + client_id: z.string().min(1, "Client ID is required"), + client_secret: z.string().min(1, "Client Secret is required"), + user_agent: z.string().min(1, "User Agent is required"), + username: z.string().optional(), + password: z.string().optional(), +}); + export const oauthSchema = z.object({}); export const kubernetesIntegrationSchema = z.object({ diff --git a/src/validations/index.ts b/src/validations/index.ts index dcc8050af9..febc7bbbc2 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -32,6 +32,7 @@ export { microsoftTeamsIntegrationSchema, linearOauthIntegrationSchema, kubernetesIntegrationSchema, + redditPrivateAuthIntegrationSchema, } from "@validations/connection.schema"; export { codeAssetsSchema } from "@validations/coseAndAssets.schema"; export { validateManualRun } from "@validations/manualRun.schema"; From 191b64bd58b25e703bc9e0abbe02a296caf70775 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Thu, 9 Oct 2025 13:24:00 +0300 Subject: [PATCH 2/8] fix: reddit connection missing forms --- .../connections/integrations/reddit/add.tsx | 26 +++++++------- .../connections/integrations/reddit/edit.tsx | 36 +++++++++++-------- .../addComponentsMapping.constants.ts | 2 ++ .../editComponentsMapping.constants.ts | 2 ++ src/locales/en/integrations/translation.json | 10 ++++++ 5 files changed, 48 insertions(+), 28 deletions(-) diff --git a/src/components/organisms/connections/integrations/reddit/add.tsx b/src/components/organisms/connections/integrations/reddit/add.tsx index 41b35cd39d..573a7b4557 100644 --- a/src/components/organisms/connections/integrations/reddit/add.tsx +++ b/src/components/organisms/connections/integrations/reddit/add.tsx @@ -18,7 +18,7 @@ export const RedditIntegrationAddForm = ({ connectionId?: string; triggerParentFormSubmit: () => void; }) => { - const { t } = useTranslation("integrations"); + const { t } = useTranslation("integrations", { keyPrefix: "reddit" }); const { createConnection, errors, handleSubmit, isLoading, register } = useConnectionForm( redditPrivateAuthIntegrationSchema, @@ -37,11 +37,11 @@ export const RedditIntegrationAddForm = ({
{errors.client_id?.message as string} @@ -50,11 +50,11 @@ export const RedditIntegrationAddForm = ({
{errors.client_secret?.message as string} @@ -63,11 +63,11 @@ export const RedditIntegrationAddForm = ({
{errors.user_agent?.message as string} @@ -76,10 +76,10 @@ export const RedditIntegrationAddForm = ({
{errors.username?.message as string} @@ -88,10 +88,10 @@ export const RedditIntegrationAddForm = ({
@@ -99,7 +99,7 @@ export const RedditIntegrationAddForm = ({
); diff --git a/src/components/organisms/connections/integrations/reddit/edit.tsx b/src/components/organisms/connections/integrations/reddit/edit.tsx index f9e693ba46..e3fcdcecef 100644 --- a/src/components/organisms/connections/integrations/reddit/edit.tsx +++ b/src/components/organisms/connections/integrations/reddit/edit.tsx @@ -13,7 +13,7 @@ import { Button, ErrorMessage, Input, SecretInput, Spinner } from "@components/a import { FloppyDiskIcon } from "@assets/image/icons"; export const RedditIntegrationEditForm = () => { - const { t } = useTranslation("integrations"); + const { t } = useTranslation("integrations", { keyPrefix: "reddit" }); const [lockState, setLockState] = useState({ client_secret: true, password: true, @@ -37,11 +37,11 @@ export const RedditIntegrationEditForm = () => {
{errors.client_id?.message as string} @@ -51,16 +51,19 @@ export const RedditIntegrationEditForm = () => { setValue("client_secret", newValue)} handleLockAction={(newLockState: boolean) => - setLockState((prevState) => ({ ...prevState, client_secret: newLockState })) + setLockState((prevState) => ({ + ...prevState, + client_secret: newLockState, + })) } isError={!!errors.client_secret} isLocked={lockState.client_secret} isRequired - label={t("reddit.placeholders.clientSecret")} + label={t("placeholders.clientSecret")} value={clientSecret} /> @@ -70,11 +73,11 @@ export const RedditIntegrationEditForm = () => {
@@ -84,10 +87,10 @@ export const RedditIntegrationEditForm = () => {
@@ -98,15 +101,18 @@ export const RedditIntegrationEditForm = () => { setValue("password", newValue)} handleLockAction={(newLockState: boolean) => - setLockState((prevState) => ({ ...prevState, password: newLockState })) + setLockState((prevState) => ({ + ...prevState, + password: newLockState, + })) } isError={!!errors.password} isLocked={lockState.password} - label={t("reddit.placeholders.password")} + label={t("placeholders.password")} value={password} /> @@ -114,14 +120,14 @@ export const RedditIntegrationEditForm = () => {
); diff --git a/src/constants/connections/addComponentsMapping.constants.ts b/src/constants/connections/addComponentsMapping.constants.ts index 3e5ff866f4..77e89f1515 100644 --- a/src/constants/connections/addComponentsMapping.constants.ts +++ b/src/constants/connections/addComponentsMapping.constants.ts @@ -24,6 +24,7 @@ import { ZoomIntegrationAddForm, SalesforceIntegrationAddForm, KubernetesIntegrationAddForm, + RedditIntegrationAddForm, } from "@components/organisms/connections/integrations"; import { MicrosoftTeamsIntegrationAddForm } from "@components/organisms/connections/integrations/microsoft/teams"; @@ -54,4 +55,5 @@ export const integrationAddFormComponents: Partial>>>>>> b065ac563 (fix: reddit connection missing forms) } } } From 098cce682b81e276d22875d78601e3244bcdbaee Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Thu, 9 Oct 2025 15:45:57 +0300 Subject: [PATCH 3/8] fix: reddit connection missing forms - re-check reddit --- src/components/atoms/hint.tsx | 19 ++++ src/components/atoms/index.ts | 1 + src/components/atoms/input.tsx | 52 ++++++----- src/components/atoms/secretInput.tsx | 87 ++++++++++--------- src/components/molecules/select/base.tsx | 55 +++++++----- .../connections/integrations/reddit/add.tsx | 48 ++++++++-- .../connections/integrations/reddit/edit.tsx | 43 ++++++++- .../integrationInfoLinks.constants.ts | 9 ++ src/constants/lists/index.ts | 1 + .../components/forms/input.interface.ts | 1 + .../components/forms/secretInput.interface.ts | 1 + .../components/forms/select.interface.ts | 1 + src/locales/en/integrations/translation.json | 13 ++- 13 files changed, 231 insertions(+), 100 deletions(-) create mode 100644 src/components/atoms/hint.tsx diff --git a/src/components/atoms/hint.tsx b/src/components/atoms/hint.tsx new file mode 100644 index 0000000000..90dc866614 --- /dev/null +++ b/src/components/atoms/hint.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import { cn } from "@utilities"; + +import { InfoIcon } from "@assets/image/icons"; + +interface HintProps { + children: React.ReactNode; + className?: string; +} + +export const Hint: React.FC = ({ children, className }) => { + return ( +
+ + {children} +
+ ); +}; diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts index 86765b6e31..b03df37766 100644 --- a/src/components/atoms/index.ts +++ b/src/components/atoms/index.ts @@ -6,6 +6,7 @@ export { Checkbox } from "@components/atoms/checkbox"; export { DropdownMenu } from "@components/atoms/dropdownMenu"; export { ErrorMessage } from "@components/atoms/errorMessage"; export { Frame } from "@components/atoms/frame"; +export { Hint } from "@components/atoms/hint"; export { IconSvg } from "@components/atoms/icons"; export { Input } from "@components/atoms/input"; export { Link } from "@components/atoms/link"; diff --git a/src/components/atoms/input.tsx b/src/components/atoms/input.tsx index c0f2074c21..fd27e6e5fe 100644 --- a/src/components/atoms/input.tsx +++ b/src/components/atoms/input.tsx @@ -4,6 +4,8 @@ import { InputVariant } from "@enums/components"; import { InputProps } from "@interfaces/components"; import { cn } from "@utilities"; +import { Hint } from "@components/atoms/hint"; + export const Input = forwardRef( ( { @@ -11,6 +13,7 @@ export const Input = forwardRef( className, defaultValue = "", disabled = false, + hint, inputLabelTextSize, icon, isError = false, @@ -105,29 +108,32 @@ export const Input = forwardRef( }); return ( -
- - {label ? ( - - ) : null} - {icon} -
+ <> +
+ + {label ? ( + + ) : null} + {icon} +
+ {hint ? {hint} : null} + ); } ); diff --git a/src/components/atoms/secretInput.tsx b/src/components/atoms/secretInput.tsx index fde36b1626..d22f57ea70 100644 --- a/src/components/atoms/secretInput.tsx +++ b/src/components/atoms/secretInput.tsx @@ -3,6 +3,7 @@ import React, { forwardRef, useCallback, useEffect, useId, useState } from "reac import { useTranslation } from "react-i18next"; import { Button } from "./buttons"; +import { Hint } from "./hint"; import { ButtonVariant, InputVariant } from "@enums/components"; import { SecretInputProps } from "@interfaces/components"; import { cn } from "@utilities"; @@ -19,6 +20,7 @@ export const SecretInput = forwardRef((props disabled, handleInputChange, handleLockAction, + hint, isError, isLocked = false, isLockedDisabled, @@ -142,50 +144,53 @@ export const SecretInput = forwardRef((props }; return ( -
-
- - - {labelText ? ( - - ) : null} - - {isLockedDisabled ? ( - + ) : null} +
+ + {!isLockedDisabled ? ( + ) : null}
- - {!isLockedDisabled ? ( - - ) : null} -
+ {hint ? {hint} : null} + ); }); diff --git a/src/components/molecules/select/base.tsx b/src/components/molecules/select/base.tsx index d9cee8c673..ed8ab42338 100644 --- a/src/components/molecules/select/base.tsx +++ b/src/components/molecules/select/base.tsx @@ -8,6 +8,7 @@ import { getSelectDarkStyles, getSelectLightStyles } from "@constants"; import { SelectOption, SelectProps } from "@interfaces/components"; import { cn } from "@utilities"; +import { Hint } from "@components/atoms"; import { IconLabel } from "@components/molecules/select"; interface BaseSelectProps extends SelectProps { @@ -23,6 +24,7 @@ export const BaseSelect = forwardRef( dataTestid, defaultValue, disabled = false, + hint, isError = false, isRequired = false, label, @@ -112,30 +114,35 @@ export const BaseSelect = forwardRef( const defaultCreateLabel = t("creatableSelectDefaultCreateLabel"); return ( -
- `${createLabel || defaultCreateLabel} "${createLabelItem}"`} - id={id} - isDisabled={disabled} - isOptionDisabled={(option: SelectOption) => !!option.disabled} - noOptionsMessage={noOptionsMessage} - onBlur={handleBlur} - onChange={handleChange} - onCreateOption={onCreateOption} - onFocus={handleFocus} - options={options} - placeholder={isRequired ? `${placeholder} *` : placeholder} - styles={selectStyles} - value={selectedOption || defaultValue} - /> - - -
+ <> +
+ + `${createLabel || defaultCreateLabel} "${createLabelItem}"` + } + id={id} + isDisabled={disabled} + isOptionDisabled={(option: SelectOption) => !!option.disabled} + noOptionsMessage={noOptionsMessage} + onBlur={handleBlur} + onChange={handleChange} + onCreateOption={onCreateOption} + onFocus={handleFocus} + options={options} + placeholder={isRequired ? `${placeholder} *` : placeholder} + styles={selectStyles} + value={selectedOption || defaultValue} + /> + + +
+ {hint ? {hint} : null} + ); } ); diff --git a/src/components/organisms/connections/integrations/reddit/add.tsx b/src/components/organisms/connections/integrations/reddit/add.tsx index 573a7b4557..54246ae6af 100644 --- a/src/components/organisms/connections/integrations/reddit/add.tsx +++ b/src/components/organisms/connections/integrations/reddit/add.tsx @@ -2,14 +2,16 @@ import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { infoRedditLinks } from "@constants/lists"; import { ConnectionAuthType } from "@src/enums"; import { Integrations } from "@src/enums/components"; import { useConnectionForm } from "@src/hooks"; import { redditPrivateAuthIntegrationSchema } from "@validations"; -import { Button, ErrorMessage, Input, Spinner } from "@components/atoms"; +import { Button, ErrorMessage, Input, Link, Spinner } from "@components/atoms"; +import { Accordion } from "@components/molecules"; -import { FloppyDiskIcon } from "@assets/image/icons"; +import { ExternalLinkIcon, FloppyDiskIcon } from "@assets/image/icons"; export const RedditIntegrationAddForm = ({ connectionId, @@ -20,11 +22,14 @@ export const RedditIntegrationAddForm = ({ }) => { const { t } = useTranslation("integrations", { keyPrefix: "reddit" }); - const { createConnection, errors, handleSubmit, isLoading, register } = useConnectionForm( + const { createConnection, errors, handleSubmit, isLoading, register, watch } = useConnectionForm( redditPrivateAuthIntegrationSchema, "create" ); + const username = watch("username"); + const password = watch("password"); + useEffect(() => { if (connectionId) { createConnection(connectionId, ConnectionAuthType.OauthPrivate, Integrations.reddit); @@ -65,6 +70,7 @@ export const RedditIntegrationAddForm = ({ {...register("user_agent")} aria-label={t("placeholders.userAgent")} disabled={isLoading} + hint={t("hints.userAgent")} isError={!!errors.user_agent} isRequired label={t("placeholders.userAgent")} @@ -75,9 +81,17 @@ export const RedditIntegrationAddForm = ({
{ + if ((value && !password) || (!value && password)) { + return "Both username and password are required when using user authentication"; + } + return true; + }, + })} aria-label={t("placeholders.username")} disabled={isLoading} + hint={t("hints.username")} isError={!!errors.username} label={t("placeholders.username")} /> @@ -87,9 +101,17 @@ export const RedditIntegrationAddForm = ({
{ + if ((value && !username) || (!value && username)) { + return "Both username and password are required when using user authentication"; + } + return true; + }, + })} aria-label={t("placeholders.password")} disabled={isLoading} + hint={t("hints.password")} isError={!!errors.password} label={t("placeholders.password")} type="password" @@ -98,6 +120,22 @@ export const RedditIntegrationAddForm = ({ {errors.password?.message as string}
+ +
+ {infoRedditLinks.map(({ text, url }, index) => ( + + {text} + + + ))} +
+
+
- +
{infoRedditLinks.map(({ text, url }, index) => ( ); diff --git a/src/components/organisms/connections/integrations/reddit/edit.tsx b/src/components/organisms/connections/integrations/reddit/edit.tsx index 5c86666736..b9bdcdba40 100644 --- a/src/components/organisms/connections/integrations/reddit/edit.tsx +++ b/src/components/organisms/connections/integrations/reddit/edit.tsx @@ -16,6 +16,7 @@ import { ExternalLinkIcon, FloppyDiskIcon } from "@assets/image/icons"; export const RedditIntegrationEditForm = () => { const { t } = useTranslation("integrations", { keyPrefix: "reddit" }); + const { t: tIntegrations } = useTranslation("integrations"); const [lockState, setLockState] = useState({ client_secret: true, password: true, @@ -138,7 +139,7 @@ export const RedditIntegrationEditForm = () => { {errors.password?.message as string}
- +
{infoRedditLinks.map(({ text, url }, index) => ( { ); From 4c0748a373dcf0b9147998ab66223d4a5a4ce2e8 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Thu, 9 Oct 2025 20:22:19 +0300 Subject: [PATCH 5/8] feat: reddit-connection - updated claude rules --- CLAUDE.md | 435 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 435 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 8453170bc0..1a67c68982 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -387,6 +387,441 @@ For each standard, explicitly state ✅ or ❌ and explain why: - **Environment issues:** Verify all required environment variables are set +--- + +## 🔌 Connection Integration Guidelines + +This section documents the complete process for creating, modifying, or fixing connection integrations based on the Reddit integration implementation. + +### Overview of Changes Made (Reddit Integration Example) + +**Branch:** `ronen/feat/reddit-connection` + +**Summary:** Added Reddit as a new connection integration with OAuth private authentication, including forms, validation, hints, translations, and comprehensive E2E tests. + +### File Structure for a New Connection + +When adding a new connection integration, you'll need to modify/create files in these locations: + +``` +src/ +├── components/organisms/connections/integrations/ +│ └── [integration-name]/ +│ ├── add.tsx # Add connection form +│ └── edit.tsx # Edit connection form +├── constants/ +│ ├── connections/ +│ │ ├── addComponentsMapping.constants.ts +│ │ ├── editComponentsMapping.constants.ts +│ │ └── integrationVariablesMapping.constants.ts +│ └── lists/connections/ +│ └── integrationInfoLinks.constants.ts +├── assets/image/icons/connections/ +│ ├── [integration-name].svg # Integration icon +│ └── index.ts # Export the icon +├── enums/components/ +│ └── connection.enum.ts # Add to Integrations enum +├── validations/ +│ ├── connection.schema.ts # Add Zod schema +│ └── index.ts # Export the schema +└── locales/en/integrations/ + └── translation.json # Add translations + +e2e/project/connections/ +└── [integration-name].spec.ts # E2E functional tests + +e2e/visual-regression/ +└── connections.spec.ts # Visual regression (generic, not integration-specific) +``` + +### Step-by-Step Implementation Guide + +#### 1. Create Form Components + +**File:** `src/components/organisms/connections/integrations/[integration-name]/add.tsx` + +**Key Points:** +- Use `useTranslation` with `keyPrefix` for integration-specific translations +- Add a second `useTranslation` hook WITHOUT `keyPrefix` for shared translations (buttons, information) +- Use `useConnectionForm` hook with your Zod schema +- Import and use `ConnectionAuthType` and `Integrations` enums +- For conditional field validation, use `register` with `validate` function, NOT Zod's `.superRefine()` + +**Example Pattern:** +```typescript +export const [Integration]AddForm = ({ connectionId, triggerParentFormSubmit }) => { + const { t } = useTranslation("integrations", { keyPrefix: "[integration]" }); + const { t: tIntegrations } = useTranslation("integrations"); // For shared keys + + const { createConnection, errors, handleSubmit, isLoading, register, watch } = + useConnectionForm([integration]Schema, "create"); + + // For conditional validation + const fieldA = watch("fieldA"); + const fieldB = watch("fieldB"); + + return ( +
+ { + if ((value && !fieldB) || (!value && fieldB)) { + return "Both fields required together"; + } + return true; + }, + })} + hint={t("hints.fieldA")} + /> + + + {/* Documentation links */} + + + +
+ ); +}; +``` + +**File:** `src/components/organisms/connections/integrations/[integration-name]/edit.tsx` + +Similar to add.tsx but: +- Use `onSubmitEdit` instead of `triggerParentFormSubmit` +- Use `useWatch` for form values +- Use `SecretInput` for sensitive fields with lock/unlock functionality +- Implement `lockState` for secret fields +- Use `setFormValues` utility with `integrationVariablesMapping` + +#### 2. Translation Keys + +**File:** `src/locales/en/integrations/translation.json` + +**Critical Rule:** The `../` relative path syntax does NOT work with `react-i18next`'s `keyPrefix` option. + +**Solution:** Use a second translation hook without `keyPrefix` for shared keys. + +**Structure:** +```json +{ + "[integration]": { + "placeholders": { + "fieldName": "Field Label" + }, + "hints": { + "fieldName": "Helpful hint text with format examples" + }, + "information": { + "apiDocumentation": "Integration API Documentation" + } + } +} +``` + +**Usage:** +- Integration-specific: `t("placeholders.fieldName")` (uses keyPrefix) +- Shared keys: `tIntegrations("buttons.saveConnection")` (no keyPrefix) +- Accordion title: `tIntegrations("information")` (no keyPrefix) + +#### 3. Validation Schema + +**File:** `src/validations/connection.schema.ts` + +**Critical Rule:** Do NOT use `.superRefine()` or `.refine()` - these wrap the schema in `ZodEffects` which is incompatible with `useConnectionForm`. + +**Correct Approach:** +```typescript +export const [integration]Schema = z.object({ + field_a: z.string().min(1, "Field A is required"), + field_b: z.string().optional(), + // For conditional validation, use register's validate option, not Zod +}); +``` + +**Wrong Approach:** +```typescript +// ❌ DON'T DO THIS - incompatible with useConnectionForm +export const [integration]Schema = z.object({...}).superRefine((data, ctx) => { + // validation logic +}); +``` + +**Export the schema:** +```typescript +// In src/validations/index.ts +export { [integration]Schema } from "./connection.schema"; +``` + +#### 4. Hint Component Integration + +**Usage:** Add `hint` prop to `Input`, `Select`, or `SecretInput` components: +```typescript + +``` + +The hint will automatically render below the input with an info icon. + +#### 5. Information Links + +**File:** `src/constants/lists/connections/integrationInfoLinks.constants.ts` + +**Pattern:** +```typescript +let info[Integration]Links: { text: string; url: string }[] = []; + +i18n.on("initialized", () => { + info[Integration]Links = [ + { + url: "https://docs.integration.com/api/", + text: t("[integration].information.apiDocumentation", { ns: "integrations" }), + }, + ]; +}); + +export { info[Integration]Links }; +``` + +#### 6. Constants Mapping + +**Add Component Mapping:** +```typescript +// src/constants/connections/addComponentsMapping.constants.ts +import { [Integration]AddForm } from "@components/organisms/connections/integrations/[integration]/add"; + +export const addComponentsMapping = { + // ... + [[integration]]: [Integration]AddForm, +}; +``` + +**Edit Component Mapping:** +```typescript +// src/constants/connections/editComponentsMapping.constants.ts +import { [Integration]EditForm } from "@components/organisms/connections/integrations/[integration]/edit"; + +export const editComponentsMapping = { + // ... + [[integration]]: [Integration]EditForm, +}; +``` + +**Variables Mapping:** +```typescript +// src/constants/connections/integrationVariablesMapping.constants.ts +export const integrationVariablesMapping = { + // ... + [integration]: { + field_a: "field_a", + field_b: "field_b", + }, +}; +``` + +#### 7. Enum Registration + +**File:** `src/enums/components/connection.enum.ts` + +Add your integration to the `Integrations` enum: +```typescript +export enum Integrations { + // ... + [integration] = "[integration]", +} +``` + +#### 8. Icon Setup + +1. Add SVG icon to `src/assets/image/icons/connections/` +2. Export it in `src/assets/image/icons/connections/index.ts` + +#### 9. E2E Testing + +**File:** `e2e/project/connections/[integration].spec.ts` + +**Critical Rules:** +- Each test must create a new project in `beforeEach` +- Run tests with `--workers=1` to avoid backend rate limiting +- Test on single browser (Chrome) to prevent simultaneous API calls +- Use `waitForToast()` for success/error validation +- Test full CRUD operations +- Remove `dashboardPage` from test parameters if not used + +**Test Structure:** +```typescript +import { expect, test } from "e2e/fixtures"; +import { waitForToast } from "e2e/utils"; + +test.describe("[Integration] Connection Suite", () => { + test.beforeEach(async ({ dashboardPage }) => { + await dashboardPage.createProjectFromMenu(); + }); + + test("Create connection with required fields", async ({ page }) => { + await page.getByRole("tab", { name: "connections" }).click(); + await page.getByRole("button", { name: "Add new" }).click(); + + await page.getByTestId("select-integration").click(); + await page.getByRole("option", { name: "[Integration]" }).click(); + + await page.getByLabel("Field A", { exact: true }).fill("test_value"); + + await page.getByRole("button", { name: "Save Connection" }).click(); + + const toast = await waitForToast(page, "Connection created successfully"); + await expect(toast).toBeVisible(); + }); + + test("Edit connection", async ({ page }) => { + // Create first, then edit + }); + + test("Delete connection", async ({ page }) => { + // Create first, then delete + }); + + test("Validation errors", async ({ page }) => { + // Test empty required fields + }); + + test("Display hints", async ({ page }) => { + // Verify hints are visible + }); +}); +``` + +**Run Tests:** +```bash +# Run with single worker and single browser to avoid rate limiting +npx playwright test e2e/project/connections/[integration].spec.ts --workers=1 --project=Chrome +``` + +#### 10. Visual Regression Testing + +**Note:** Visual regression tests were initially created but removed due to backend performance and timeout issues during project creation. They can be added back in the future when the backend is more stable. + +If you want to add visual regression tests in the future: +- Create them in `e2e/visual-regression/` folder +- They must run within a project context (extract `projectId` from URL after creation) +- Keep them GENERIC - test overall connections page, not specific integrations +- Consider using `beforeAll` with a shared project instead of `beforeEach` to avoid rate limits +- Use `--workers=1` to prevent backend rate limiting + +**Example structure:** +```typescript +test.describe("Connections Visual Regression", () => { + let projectId: string; + + test.beforeAll(async ({ dashboardPage, page }) => { + await dashboardPage.createProjectFromMenu(); + projectId = page.url().match(/\/projects\/([^/]+)/)?.[1] || ""; + }); + + test("List view", async ({ page }) => { + await page.goto(`/projects/${projectId}/connections`); + await expect(page).toHaveScreenshot("connections-list.png"); + }); +}); +``` + +### Common Issues and Solutions + +#### Issue 1: Translation Keys Not Working +**Problem:** Seeing literal translation keys like "reddit.../buttons.saveConnection" +**Solution:** Don't use `../` with keyPrefix. Use separate translation hook: +```typescript +const { t } = useTranslation("integrations", { keyPrefix: "reddit" }); +const { t: tIntegrations } = useTranslation("integrations"); +// Use tIntegrations for shared keys +``` + +#### Issue 2: Zod Schema Type Error +**Problem:** `Argument of type 'ZodEffects<...>' is not assignable to parameter of type 'ZodObject<...>'` +**Solution:** Don't use `.superRefine()` or `.refine()`. Use `register`'s `validate` option instead. + +#### Issue 3: E2E Tests Timing Out +**Problem:** Tests fail with 3-minute timeout +**Solution:** Backend rate limiting from parallel tests. Run with `--workers=1 --project=Chrome` + +#### Issue 4: Conditional Validation +**Problem:** Need to validate that two fields are required together +**Solution:** Use react-hook-form's `validate` with `watch`: +```typescript +const fieldB = watch("fieldB"); + { + if ((value && !fieldB) || (!value && fieldB)) { + return "Both fields required"; + } + return true; + }, + })} +/> +``` + +### Quality Checklist + +Before submitting a PR for a new connection integration: + +- [ ] **Forms Created:** Both add.tsx and edit.tsx implemented +- [ ] **Translations Added:** All placeholders, hints, and information links +- [ ] **Translation Hooks:** Using both prefixed and non-prefixed hooks correctly +- [ ] **Schema Defined:** Zod schema without .superRefine()/.refine() +- [ ] **Validation:** Conditional validation using register's validate option +- [ ] **Hints Added:** Helpful hints for complex fields +- [ ] **Constants Mapped:** addComponentsMapping, editComponentsMapping, variablesMapping +- [ ] **Enum Updated:** Integration added to Integrations enum +- [ ] **Icon Added:** SVG icon added and exported +- [ ] **Info Links:** Documentation links configured in integrationInfoLinks +- [ ] **E2E Tests:** Comprehensive tests covering CRUD operations +- [ ] **Tests Pass:** All tests run successfully with `--workers=1 --project=Chrome` +- [ ] **No Comments:** All code files are clean without comments +- [ ] **Linting:** `npm run lint:fix` passes +- [ ] **Type Check:** `npm run tsc` passes +- [ ] **Build:** `npm run build` completes successfully + +### Files Modified in Reddit Integration Example + +**Created:** +- `src/components/organisms/connections/integrations/reddit/add.tsx` +- `src/components/organisms/connections/integrations/reddit/edit.tsx` +- `src/components/atoms/hint.tsx` +- `e2e/project/connections/reddit.spec.ts` + +**Modified:** +- `src/locales/en/integrations/translation.json` - Added reddit translations +- `src/constants/connections/addComponentsMapping.constants.ts` - Mapped Reddit add form +- `src/constants/connections/editComponentsMapping.constants.ts` - Mapped Reddit edit form +- `src/constants/connections/integrationVariablesMapping.constants.ts` - Added reddit field mapping +- `src/constants/lists/connections/integrationInfoLinks.constants.ts` - Added Reddit API docs +- `src/constants/lists/index.ts` - Exported infoRedditLinks +- `src/enums/components/connection.enum.ts` - Added reddit to Integrations enum +- `src/validations/connection.schema.ts` - Added redditPrivateAuthIntegrationSchema +- `src/validations/index.ts` - Exported reddit schema +- `src/components/organisms/connections/integrations/index.ts` - Exported Reddit forms +- `src/components/atoms/index.ts` - Exported Hint component +- `src/interfaces/components/forms/input.interface.ts` - Added hint prop +- `src/interfaces/components/forms/select.interface.ts` - Added hint prop +- `src/interfaces/components/forms/secretInput.interface.ts` - Added hint prop +- `src/components/atoms/input.tsx` - Integrated Hint component +- `src/components/atoms/secretInput.tsx` - Integrated Hint component +- `src/components/molecules/select/base.tsx` - Integrated Hint component + +### Key Insights + +1. **Translation Architecture:** react-i18next's `keyPrefix` doesn't support `../` relative paths. Use multiple translation hooks. +2. **Validation Strategy:** Keep Zod schemas simple. Use react-hook-form's built-in validation for conditional logic. +3. **Testing Strategy:** Separate concerns - functional E2E tests per integration, generic visual regression tests. +4. **Test Execution:** Always run with `--workers=1 --project=Chrome` to avoid backend rate limiting. +5. **Component Reusability:** The Hint component pattern provides consistent UX across all integrations. +6. **No Comments Policy:** Code should be self-documenting. Only add comments when explicitly requested. + --- ## 🧠 Claude Optimized Labels From 1e51b39cf9738affe018371f873e78f150013a75 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Sun, 12 Oct 2025 15:24:13 +0300 Subject: [PATCH 6/8] fix: save bugs username without password and vice-versa --- .../connections/integrations/reddit/add.tsx | 26 +++--------- .../connections/integrations/reddit/edit.tsx | 30 +++++--------- src/hooks/index.ts | 1 + src/hooks/useConnectionForm.ts | 18 ++++---- src/hooks/useCrossFieldValidation.ts | 29 +++++++++++++ src/locales/en/integrations/translation.json | 3 ++ .../flattenFormDataWithZodValidation.utils.ts | 8 +++- src/validations/connection.schema.ts | 41 ++++++++++++++++++- 8 files changed, 106 insertions(+), 50 deletions(-) create mode 100644 src/hooks/useCrossFieldValidation.ts diff --git a/src/components/organisms/connections/integrations/reddit/add.tsx b/src/components/organisms/connections/integrations/reddit/add.tsx index 51fbc3c78b..696fe54e46 100644 --- a/src/components/organisms/connections/integrations/reddit/add.tsx +++ b/src/components/organisms/connections/integrations/reddit/add.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { infoRedditLinks } from "@constants/lists"; import { ConnectionAuthType } from "@src/enums"; import { Integrations } from "@src/enums/components"; -import { useConnectionForm } from "@src/hooks"; +import { useConnectionForm, useCrossFieldValidation } from "@src/hooks"; import { redditPrivateAuthIntegrationSchema } from "@validations"; import { Button, ErrorMessage, Input, Link, Spinner } from "@components/atoms"; @@ -23,13 +23,13 @@ export const RedditIntegrationAddForm = ({ const { t } = useTranslation("integrations", { keyPrefix: "reddit" }); const { t: tIntegrations } = useTranslation("integrations"); - const { createConnection, errors, handleSubmit, isLoading, register, watch } = useConnectionForm( + const { createConnection, errors, handleSubmit, isLoading, register, trigger } = useConnectionForm( redditPrivateAuthIntegrationSchema, "create" ); - const username = watch("username"); - const password = watch("password"); + const handleUsernameChange = useCrossFieldValidation(trigger, ["password"]); + const handlePasswordChange = useCrossFieldValidation(trigger, ["username"]); useEffect(() => { if (connectionId) { @@ -82,14 +82,7 @@ export const RedditIntegrationAddForm = ({
{ - if ((value && !password) || (!value && password)) { - return "Both username and password are required when using user authentication"; - } - return true; - }, - })} + {...register("username", { onChange: handleUsernameChange })} aria-label={t("placeholders.username")} disabled={isLoading} hint={t("hints.username")} @@ -102,14 +95,7 @@ export const RedditIntegrationAddForm = ({
{ - if ((value && !username) || (!value && username)) { - return "Both username and password are required when using user authentication"; - } - return true; - }, - })} + {...register("password", { onChange: handlePasswordChange })} aria-label={t("placeholders.password")} disabled={isLoading} hint={t("hints.password")} diff --git a/src/components/organisms/connections/integrations/reddit/edit.tsx b/src/components/organisms/connections/integrations/reddit/edit.tsx index b9bdcdba40..96f53eedd7 100644 --- a/src/components/organisms/connections/integrations/reddit/edit.tsx +++ b/src/components/organisms/connections/integrations/reddit/edit.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import { infoRedditLinks } from "@constants/lists"; import { integrationVariablesMapping } from "@src/constants"; -import { useConnectionForm } from "@src/hooks"; +import { useConnectionForm, useCrossFieldValidation } from "@src/hooks"; import { setFormValues } from "@src/utilities"; import { redditPrivateAuthIntegrationSchema } from "@validations"; @@ -21,7 +21,7 @@ export const RedditIntegrationEditForm = () => { client_secret: true, password: true, }); - const { connectionVariables, control, errors, handleSubmit, isLoading, onSubmitEdit, register, setValue } = + const { connectionVariables, control, errors, handleSubmit, isLoading, onSubmitEdit, register, setValue, trigger } = useConnectionForm(redditPrivateAuthIntegrationSchema, "edit"); const clientId = useWatch({ control, name: "client_id" }); @@ -30,6 +30,9 @@ export const RedditIntegrationEditForm = () => { const username = useWatch({ control, name: "username" }); const password = useWatch({ control, name: "password" }); + const handleUsernameChange = useCrossFieldValidation(trigger, ["password"]); + const handlePasswordChange = useCrossFieldValidation(trigger, ["username"]); + useEffect(() => { setFormValues(connectionVariables, integrationVariablesMapping.reddit, setValue); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -90,14 +93,7 @@ export const RedditIntegrationEditForm = () => {
{ - if ((value && !password) || (!value && password)) { - return "Both username and password are required when using user authentication"; - } - return true; - }, - })} + {...register("username", { onChange: handleUsernameChange })} aria-label={t("placeholders.username")} disabled={isLoading} hint={t("hints.username")} @@ -112,17 +108,13 @@ export const RedditIntegrationEditForm = () => {
{ - if ((value && !username) || (!value && username)) { - return "Both username and password are required when using user authentication"; - } - return true; - }, - })} + {...register("password", { onChange: handlePasswordChange })} aria-label={t("placeholders.password")} disabled={isLoading} - handleInputChange={(newValue) => setValue("password", newValue)} + handleInputChange={(newValue) => { + setValue("password", newValue); + handlePasswordChange(); + }} handleLockAction={(newLockState: boolean) => setLockState((prevState) => ({ ...prevState, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index e868f032f4..96934b702a 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,5 +1,6 @@ export { useFetchTrigger } from "./triggers/useFetchTrigger"; export { useConnectionForm } from "./useConnectionForm"; +export { useCrossFieldValidation } from "./useCrossFieldValidation"; export { useCreateProjectFromTemplate } from "./useCreateProjectFromTemplate"; export { useUserTracking } from "./useUserTracking"; export { useHubspot } from "./useHubspot"; diff --git a/src/hooks/useConnectionForm.ts b/src/hooks/useConnectionForm.ts index a5a6ceb7e4..1aba6d6577 100644 --- a/src/hooks/useConnectionForm.ts +++ b/src/hooks/useConnectionForm.ts @@ -6,7 +6,7 @@ import { FieldValues, UseFormGetValues, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { SingleValue } from "react-select"; -import { ZodObject, ZodRawShape } from "zod"; +import { ZodEffects, ZodObject, ZodRawShape, ZodSchema } from "zod"; import { ConnectionService, HttpService, LoggerService, VariablesService } from "@services"; import { namespaces } from "@src/constants"; @@ -34,12 +34,12 @@ const GoogleIntegrationsPrefixRequired = [ Integrations.forms, ]; -export const useConnectionForm = (validationSchema: ZodObject, mode: FormMode) => { +export const useConnectionForm = (validationSchema: ZodSchema, mode: FormMode) => { const { connectionId: paramConnectionId, projectId } = useParams(); const [connectionIntegrationName, setConnectionIntegrationName] = useState(); const navigate = useNavigate(); const apiBaseUrl = getApiBaseUrl(); - const [formSchema, setFormSchema] = useState>(validationSchema); + const [formSchema, setFormSchema] = useState(validationSchema); const { startCheckingStatus, setConnectionInProgress, connectionInProgress: isLoading } = useConnectionStore(); const { fetchConnections } = useCacheStore(); const { @@ -51,6 +51,7 @@ export const useConnectionForm = (validationSchema: ZodObject, mode register, reset, setValue, + trigger, watch, } = useForm({ resolver: zodResolver(formSchema), @@ -115,7 +116,7 @@ export const useConnectionForm = (validationSchema: ZodObject, mode const getFormattedConnectionData = ( getValues: UseFormGetValues, - formSchema: ZodObject, + formSchema: ZodObject | ZodEffects, integrationName?: string ) => { const connectionData = flattenFormData(getValues(), formSchema); @@ -158,7 +159,7 @@ export const useConnectionForm = (validationSchema: ZodObject, mode const { connectionData, formattedIntegrationName } = getFormattedConnectionData( getValues, - formSchema, + formSchema as ZodObject | ZodEffects, integrationName ); @@ -219,7 +220,7 @@ export const useConnectionForm = (validationSchema: ZodObject, mode const { connectionData, formattedIntegrationName } = getFormattedConnectionData( getValues, - formSchema, + formSchema as ZodObject | ZodEffects, integrationName! ); @@ -446,7 +447,7 @@ export const useConnectionForm = (validationSchema: ZodObject, mode const { connectionData, formattedIntegrationName } = getFormattedConnectionData( getValues, - formSchema, + formSchema as ZodObject | ZodEffects, integrationName ); const urlParams = getSpecificParams( @@ -501,7 +502,7 @@ export const useConnectionForm = (validationSchema: ZodObject, mode // eslint-disable-next-line react-hooks/exhaustive-deps }, [connectionId]); - const setValidationSchema = (newSchema: ZodObject) => { + const setValidationSchema = (newSchema: ZodSchema) => { setFormSchema(newSchema); }; @@ -532,5 +533,6 @@ export const useConnectionForm = (validationSchema: ZodObject, mode clearErrors, handleCustomOauth, setConnectionType, + trigger, }; }; diff --git a/src/hooks/useCrossFieldValidation.ts b/src/hooks/useCrossFieldValidation.ts new file mode 100644 index 0000000000..ab38bd10f8 --- /dev/null +++ b/src/hooks/useCrossFieldValidation.ts @@ -0,0 +1,29 @@ +import { UseFormTrigger, FieldValues, Path } from "react-hook-form"; + +/** + * Creates onChange handlers for cross-field validation. + * When one field changes, it automatically triggers validation on the related field(s). + * + * @param trigger - The trigger function from react-hook-form + * @param fieldName - The name of the current field + * @param relatedFields - Array of field names that should be re-validated when this field changes + * @returns An onChange handler that triggers validation on related fields + * + * @example + * ```tsx + * const { register, trigger } = useForm(); + * const handleUsernameChange = useCrossFieldValidation(trigger, 'username', ['password']); + * + * + * ``` + */ +export const useCrossFieldValidation = ( + trigger: UseFormTrigger, + relatedFields: Path[] +) => { + return () => { + relatedFields.forEach((field) => { + trigger(field); + }); + }; +}; diff --git a/src/locales/en/integrations/translation.json b/src/locales/en/integrations/translation.json index e43f3a5ae8..678ca95b3f 100644 --- a/src/locales/en/integrations/translation.json +++ b/src/locales/en/integrations/translation.json @@ -267,6 +267,9 @@ "username": "Optional: Reddit username (without u/ prefix) for user authentication", "password": "Optional: Reddit account password (required if username is provided)" }, + "errors": { + "usernamePasswordRequired": "Both username and password are required when using user authentication" + }, "information": { "apiDocumentation": "Reddit API Documentation" } diff --git a/src/utilities/flattenFormDataWithZodValidation.utils.ts b/src/utilities/flattenFormDataWithZodValidation.utils.ts index b8a26eeb19..9c43725116 100644 --- a/src/utilities/flattenFormDataWithZodValidation.utils.ts +++ b/src/utilities/flattenFormDataWithZodValidation.utils.ts @@ -8,13 +8,17 @@ const getInnerSchema = (schema: ZodTypeAny): ZodTypeAny => { return schema; }; -export const flattenFormData = >(formData: any, schema: T): Record => { +export const flattenFormData = | ZodEffects>( + formData: any, + schema: T +): Record => { const result: Record = {}; + const innerSchema = getInnerSchema(schema as ZodTypeAny) as ZodObject; for (const key in formData) { if (Object.prototype.hasOwnProperty.call(formData, key)) { const value = formData[key]; - const schemaField = getInnerSchema(schema.shape[key]); + const schemaField = getInnerSchema(innerSchema.shape[key]); if (schemaField instanceof ZodObject && "label" in schemaField.shape && "value" in schemaField.shape) { result[key] = value.value; diff --git a/src/validations/connection.schema.ts b/src/validations/connection.schema.ts index 626339d403..8be6bc8864 100644 --- a/src/validations/connection.schema.ts +++ b/src/validations/connection.schema.ts @@ -1,3 +1,4 @@ +import i18n, { t } from "i18next"; import { z } from "zod"; import { ValidateDomain } from "@src/utilities"; @@ -173,7 +174,7 @@ export const microsoftTeamsIntegrationSchema = z.object({ tenant_id: z.string().min(1, "Tenant ID is required"), }); -export const redditPrivateAuthIntegrationSchema = z.object({ +const baseRedditSchema = z.object({ client_id: z.string().min(1, "Client ID is required"), client_secret: z.string().min(1, "Client Secret is required"), user_agent: z.string().min(1, "User Agent is required"), @@ -181,6 +182,44 @@ export const redditPrivateAuthIntegrationSchema = z.object({ password: z.string().optional(), }); +const createRedditSchemaWithValidation = (errorMessage: string) => + baseRedditSchema.superRefine((data, ctx) => { + const hasUsername = data.username && data.username.trim().length > 0; + const hasPassword = data.password && data.password.trim().length > 0; + + if (hasUsername !== hasPassword) { + if (!hasPassword) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: errorMessage, + path: ["password"], + }); + } + + if (!hasUsername) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: errorMessage, + path: ["username"], + }); + } + } + }); + +const fallbackRedditSchema = createRedditSchemaWithValidation( + "Both username and password are required when using user authentication" +); + +let redditPrivateAuthIntegrationSchema = fallbackRedditSchema; + +i18n.on("initialized", () => { + redditPrivateAuthIntegrationSchema = createRedditSchemaWithValidation( + t("reddit.errors.usernamePasswordRequired", { ns: "integrations" }) + ); +}); + +export { redditPrivateAuthIntegrationSchema }; + export const oauthSchema = z.object({}); export const kubernetesIntegrationSchema = z.object({ From 7e72ea5a9b1f8920bbb8f4aa6921a2d9139182a6 Mon Sep 17 00:00:00 2001 From: Ronen Mars Date: Sun, 12 Oct 2025 15:59:38 +0300 Subject: [PATCH 7/8] fix: margin from the top --- .../organisms/connections/integrations/reddit/add.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/organisms/connections/integrations/reddit/add.tsx b/src/components/organisms/connections/integrations/reddit/add.tsx index 696fe54e46..69f863ff75 100644 --- a/src/components/organisms/connections/integrations/reddit/add.tsx +++ b/src/components/organisms/connections/integrations/reddit/add.tsx @@ -107,7 +107,7 @@ export const RedditIntegrationAddForm = ({ {errors.password?.message as string}
- +
{infoRedditLinks.map(({ text, url }, index) => ( Date: Sun, 12 Oct 2025 16:00:17 +0300 Subject: [PATCH 8/8] fix: margin from the top --- .../organisms/connections/integrations/reddit/edit.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/organisms/connections/integrations/reddit/edit.tsx b/src/components/organisms/connections/integrations/reddit/edit.tsx index 96f53eedd7..8b43efce1f 100644 --- a/src/components/organisms/connections/integrations/reddit/edit.tsx +++ b/src/components/organisms/connections/integrations/reddit/edit.tsx @@ -131,7 +131,7 @@ export const RedditIntegrationEditForm = () => { {errors.password?.message as string}
- +
{infoRedditLinks.map(({ text, url }, index) => (