From 3ba3349cf427586dc841b5271b5f866bdf862b53 Mon Sep 17 00:00:00 2001 From: Tofik Hasanov Date: Tue, 27 Jan 2026 16:35:04 -0500 Subject: [PATCH] feat(integrations): add validation for target repositories in integration dialog --- .../integrations/ManageIntegrationDialog.tsx | 212 +++++++++++------- 1 file changed, 130 insertions(+), 82 deletions(-) diff --git a/apps/app/src/components/integrations/ManageIntegrationDialog.tsx b/apps/app/src/components/integrations/ManageIntegrationDialog.tsx index 18dfa50ae..5eff646cc 100644 --- a/apps/app/src/components/integrations/ManageIntegrationDialog.tsx +++ b/apps/app/src/components/integrations/ManageIntegrationDialog.tsx @@ -87,6 +87,26 @@ interface ManageIntegrationDialogProps { onSaved?: () => void; } +const validateTargetRepos = ( + values: Record, +): boolean => { + const targetReposValue = values.target_repos; + if (!Array.isArray(targetReposValue) || targetReposValue.length === 0) { + return true; + } + for (const value of targetReposValue) { + const colonIndex = String(value).lastIndexOf(':'); + if (colonIndex <= 0) { + return false; + } + const branch = String(value).substring(colonIndex + 1).trim(); + if (!branch) { + return false; + } + } + return true; +}; + export function ManageIntegrationDialog({ open, onOpenChange, @@ -322,9 +342,14 @@ export function ManageIntegrationDialog({ setDynamicOptions({}); }; + const hasVariables = variables.length > 0; + const hasCredentials = authStrategy === 'custom' && credentialFields.length > 0; + const showTabs = hasVariables && hasCredentials; + const isTargetReposValid = validateTargetRepos(variableValues); + return ( - +
@@ -350,28 +375,43 @@ export function ManageIntegrationDialog({ - {loadingVariables ? ( -
- -
- ) : ( - + {loadingVariables ? ( +
+ +
+ ) : ( + + )} +
+ + {!loadingVariables && ( + )} @@ -428,14 +468,12 @@ function ConfigurationContent({ dynamicOptions, loadingDynamicOptions, fetchDynamicOptions, - savingVariables, - handleSaveVariables, credentialFields, credentialValues, setCredentialValues, - savingCredentials, - handleSaveCredentials, - authStrategy, + hasVariables, + hasCredentials, + showTabs, activeTab, setActiveTab, }: { @@ -447,44 +485,15 @@ function ConfigurationContent({ dynamicOptions: Record; loadingDynamicOptions: Record; fetchDynamicOptions: (variableId: string) => void; - savingVariables: boolean; - handleSaveVariables: () => void; credentialFields: CredentialField[]; credentialValues: Record; setCredentialValues: React.Dispatch>>; - savingCredentials: boolean; - handleSaveCredentials: () => void; - authStrategy: string; + hasVariables: boolean; + hasCredentials: boolean; + showTabs: boolean; activeTab: 'variables' | 'credentials'; setActiveTab: (tab: 'variables' | 'credentials') => void; }) { - const hasVariables = variables.length > 0; - const hasCredentials = authStrategy === 'custom' && credentialFields.length > 0; - const showTabs = hasVariables && hasCredentials; - - // Validate target_repos - each repo must have at least one branch - const validateTargetRepos = (): boolean => { - const targetReposValue = variableValues.target_repos; - if (!Array.isArray(targetReposValue) || targetReposValue.length === 0) { - return true; // No repos selected is OK (will be caught by required check) - } - // Check that each repo has a branch specified - for (const value of targetReposValue) { - const colonIndex = String(value).lastIndexOf(':'); - if (colonIndex <= 0) { - // No colon means no branch specified - return false; - } - const branch = String(value).substring(colonIndex + 1).trim(); - if (!branch) { - return false; - } - } - return true; - }; - - const isTargetReposValid = validateTargetRepos(); - // If neither available, show empty state if (!hasVariables && !hasCredentials) { return ( @@ -601,21 +610,6 @@ function ConfigurationContent({ ); })} - - ); @@ -723,17 +717,6 @@ function ConfigurationContent({ )} ))} - - ); @@ -765,6 +748,71 @@ function ConfigurationContent({ return
{variablesContent || credentialsContent}
; } +function ConfigurationFooterActions({ + hasVariables, + hasCredentials, + showTabs, + activeTab, + savingVariables, + handleSaveVariables, + isTargetReposValid, + savingCredentials, + handleSaveCredentials, + showActionsFooter, +}: { + hasVariables: boolean; + hasCredentials: boolean; + showTabs: boolean; + activeTab: 'variables' | 'credentials'; + savingVariables: boolean; + handleSaveVariables: () => void; + isTargetReposValid: boolean; + savingCredentials: boolean; + handleSaveCredentials: () => void; + showActionsFooter: boolean; +}) { + if (!hasVariables && !hasCredentials) { + return null; + } + + const showVariablesButton = hasVariables && (!showTabs || activeTab === 'variables'); + const showCredentialsButton = hasCredentials && (!showTabs || activeTab === 'credentials'); + const footerClassName = showActionsFooter ? 'pt-4' : 'border-t pt-4'; + + return ( +
+ {showVariablesButton && ( + + )} + {showCredentialsButton && ( + + )} +
+ ); +} + /** * Parse a stored value like "owner/repo:branch" into parts. * Handles trailing colons, empty branches, and non-string values.