From 981cdc1961e1c328dec80c5f9b2e0fca1251bbe7 Mon Sep 17 00:00:00 2001 From: Sourabh Sen Date: Mon, 11 May 2026 11:22:33 +0530 Subject: [PATCH 1/5] [ENG-295] Support dynamic type alert in configFields --- .../components/IntegrationFields.tsx | 40 +++++++++++++++++-- src/modules/integration-picker/types.ts | 3 +- .../integration-picker/utils/zodSchema.ts | 1 + 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/modules/integration-picker/components/IntegrationFields.tsx b/src/modules/integration-picker/components/IntegrationFields.tsx index d960ad4..fe8f0b7 100644 --- a/src/modules/integration-picker/components/IntegrationFields.tsx +++ b/src/modules/integration-picker/components/IntegrationFields.tsx @@ -15,7 +15,7 @@ import { TextArea, Typography, } from '@stackone/malachite'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { FieldErrors, UseFormSetValue } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import useDeepCompareEffect from 'use-deep-compare-effect'; @@ -144,6 +144,16 @@ const FieldRenderer: React.FC = ({ ); } + if (field.type === 'alert') { + return ( + + ); + } + if (field.type === 'text_area') { return ( <> @@ -229,7 +239,8 @@ interface IntegrationFieldsProps { const NoFieldsView: React.FC<{ integrationName: string; error?: { message: string; provider_response: string }; -}> = ({ integrationName, error }) => { + alertFields?: ConnectorConfigField[]; +}> = ({ integrationName, error, alertFields }) => { return ( {error && ( @@ -241,6 +252,18 @@ const NoFieldsView: React.FC<{ )} + {alertFields && alertFields.length > 0 && ( + + {alertFields.map((field) => ( + + ))} + + )} = ({ onValidationChange?.(isValid); }, [isValid, onValidationChange]); - if (fields.length === 0) { - return ; + const alertOnlyFields = fields.filter((f) => f.type === 'alert'); + const hasInputFields = fields.some((f) => f.type !== 'alert'); + + if (!hasInputFields) { + return ( + + ); } return ( diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 950809c..08f908c 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -18,7 +18,8 @@ export interface HubData { } export interface ConnectorConfigField { - type?: 'text' | 'password' | 'number' | 'select' | 'text_area'; + type?: 'text' | 'password' | 'number' | 'select' | 'text_area' | 'alert'; + alertType?: 'warning' | 'info'; label: string; key: string; required: boolean; diff --git a/src/modules/integration-picker/utils/zodSchema.ts b/src/modules/integration-picker/utils/zodSchema.ts index 6e399da..5c61c25 100644 --- a/src/modules/integration-picker/utils/zodSchema.ts +++ b/src/modules/integration-picker/utils/zodSchema.ts @@ -55,6 +55,7 @@ export function createFormSchema(fields: ConnectorConfigField[]) { const schemaShape: Record = {}; for (const field of fields) { + if (field.type === 'alert') continue; schemaShape[field.key] = createFieldSchema(field); } From 468d02806b249c747ecdf9a46f5b329f2ba39ee0 Mon Sep 17 00:00:00 2001 From: Sourabh Sen Date: Wed, 13 May 2026 13:44:57 +0530 Subject: [PATCH 2/5] Updates for alert block with AuthenticationNotice type --- .../integration-picker/IntegrationPicker.tsx | 2 + .../components/IntegrationFields.tsx | 111 ++++++++--------- .../components/IntegrationPickerContent.tsx | 11 +- .../components/views/IntegrationFormView.tsx | 5 +- .../hooks/useIntegrationPicker.ts | 113 +++++++++--------- src/modules/integration-picker/types.ts | 14 ++- .../utils/partitionNotices.ts | 20 ++++ .../integration-picker/utils/zodSchema.ts | 1 - 8 files changed, 162 insertions(+), 115 deletions(-) create mode 100644 src/modules/integration-picker/utils/partitionNotices.ts diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index 0f4a945..157b525 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -41,6 +41,7 @@ export const IntegrationPicker: React.FC = ({ connectorData, selectedIntegration, fields, + notices, guide, // State @@ -152,6 +153,7 @@ export const IntegrationPicker: React.FC = ({ connectorData={connectorData?.config ?? null} hubData={hubData ?? null} fields={fields} + notices={notices} errorHubData={(errorHubData as Error) ?? null} errorConnectorData={(errorConnectorData as Error) ?? null} onSelect={setSelectedIntegration} diff --git a/src/modules/integration-picker/components/IntegrationFields.tsx b/src/modules/integration-picker/components/IntegrationFields.tsx index fe8f0b7..b7c0967 100644 --- a/src/modules/integration-picker/components/IntegrationFields.tsx +++ b/src/modules/integration-picker/components/IntegrationFields.tsx @@ -19,7 +19,8 @@ import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { FieldErrors, UseFormSetValue } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import useDeepCompareEffect from 'use-deep-compare-effect'; -import { ConnectorConfigField } from '../types'; +import { AuthenticationNotice, ConnectorConfigField } from '../types'; +import { partitionNotices } from '../utils/partitionNotices'; import { formatSecretPlaceholder, isSecretPlaceholder } from '../utils/secretPlaceholder'; import { createFormSchema } from '../utils/zodSchema'; @@ -144,16 +145,6 @@ const FieldRenderer: React.FC = ({ ); } - if (field.type === 'alert') { - return ( - - ); - } - if (field.type === 'text_area') { return ( <> @@ -225,6 +216,7 @@ const ErrorBlock = ({ error }: { error?: { message: string; provider_response: s }; interface IntegrationFieldsProps { fields: Array; + notices?: Array; error?: { message: string; provider_response: string; @@ -239,8 +231,9 @@ interface IntegrationFieldsProps { const NoFieldsView: React.FC<{ integrationName: string; error?: { message: string; provider_response: string }; - alertFields?: ConnectorConfigField[]; -}> = ({ integrationName, error, alertFields }) => { + notices?: AuthenticationNotice[]; +}> = ({ integrationName, error, notices = [] }) => { + const topNotices = notices.filter((n) => !n.position || n.position === 'top'); return ( {error && ( @@ -252,18 +245,9 @@ const NoFieldsView: React.FC<{ )} - {alertFields && alertFields.length > 0 && ( - - {alertFields.map((field) => ( - - ))} - - )} + {topNotices.map((n) => ( + + ))} = ({ fields, + notices = [], onChange, error, onValidationChange, @@ -295,6 +280,11 @@ export const IntegrationForm: React.FC = ({ editingSecrets, setEditingSecrets, }) => { + const displayedFields = fields.filter((f) => f.display !== false); + const fieldKeys = displayedFields.map((f) => + typeof f.key === 'object' ? JSON.stringify(f.key) : String(f.key), + ); + const { noticesBefore, noticesAfter } = partitionNotices(notices, fieldKeys); const schema = useMemo(() => createFormSchema(fields), [fields]); const defaultValues = useMemo(() => { @@ -331,17 +321,8 @@ export const IntegrationForm: React.FC = ({ onValidationChange?.(isValid); }, [isValid, onValidationChange]); - const alertOnlyFields = fields.filter((f) => f.type === 'alert'); - const hasInputFields = fields.some((f) => f.type !== 'alert'); - - if (!hasInputFields) { - return ( - - ); + if (fields.length === 0) { + return ; } return ( @@ -353,25 +334,47 @@ export const IntegrationForm: React.FC = ({ )}
- {fields - .filter((field) => field.display !== false) - .map((field) => { - const key = - typeof field.key === 'object' - ? JSON.stringify(field.key) - : String(field.key); - return ( -
- { + const key = + typeof field.key === 'object' + ? JSON.stringify(field.key) + : String(field.key); + return ( +
+ {noticesBefore(key).map((n) => ( + + ))} + + {noticesAfter(key).map((n) => ( + -
- ); - })} + ))} +
+ ); + })}
diff --git a/src/modules/integration-picker/components/IntegrationPickerContent.tsx b/src/modules/integration-picker/components/IntegrationPickerContent.tsx index a7772eb..91fe11d 100644 --- a/src/modules/integration-picker/components/IntegrationPickerContent.tsx +++ b/src/modules/integration-picker/components/IntegrationPickerContent.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { ConnectorConfig, ConnectorConfigField, HubData, Integration } from '../types'; +import { + AuthenticationNotice, + ConnectorConfig, + ConnectorConfigField, + HubData, + Integration, +} from '../types'; import { ErrorView } from './views/ErrorView'; import { IntegrationFormView } from './views/IntegrationFormView'; import { IntegrationListView } from './views/IntegrationListView'; @@ -24,6 +30,7 @@ interface IntegrationPickerContentProps { connectorData: ConnectorConfig | null; hubData: HubData | null; fields: ConnectorConfigField[]; + notices?: AuthenticationNotice[]; selectedCategory: string | null; search: string; @@ -48,6 +55,7 @@ export const IntegrationPickerContent: React.FC = connectorData, hubData, fields, + notices, selectedCategory, search, errorHubData, @@ -113,6 +121,7 @@ export const IntegrationPickerContent: React.FC = return ( = ({ fields, + notices, error, onChange, onValidationChange, @@ -27,6 +29,7 @@ export const IntegrationFormView: React.FC = ({ return ( { + const { fields, guide, notices } = useMemo(() => { if (!connectorData || !selectedIntegration) { const fields: ConnectorConfigField[] = []; - return { fields }; + const notices: AuthenticationNotice[] = []; + return { fields, notices }; } if (isFalconConnectorConfig(connectorData.config)) { - const fieldsWithPrefilledValues: ConnectorConfigField[] = - connectorData.config.configFields - .map((field) => { - const setupValue = accountData?.setupInformation?.[field.key]; + const fieldsWithPrefilledValues: ConnectorConfigField[] = ( + connectorData.config.configFields ?? [] + ) + .map((field) => { + const setupValue = accountData?.setupInformation?.[field.key]; - if (field.key === 'external-trigger-token') { - return { - ...field, - key: field.key, - value: hubData?.external_trigger_token, - }; - } + if (field.key === 'external-trigger-token') { + return { + ...field, + key: field.key, + value: hubData?.external_trigger_token, + }; + } - if (accountData && (field.secret !== false || field.type === 'password')) { - const secretValue = accountData.secrets?.[field.key]; - if (secretValue) { - return { - ...field, - key: field.key, - value: secretValue, - }; - } + if (accountData && (field.secret !== false || field.type === 'password')) { + const secretValue = accountData.secrets?.[field.key]; + if (secretValue) { return { ...field, key: field.key, - value: '', + value: secretValue, }; } - - const evaluationContext = { - ...formData, - ...accountData?.setupInformation, - external_trigger_token: hubData?.external_trigger_token, - webhooks_url: hubData?.webhooks_url, - events_encoded_context: hubData?.events_encoded_context, - hub_settings: connectorData.hub_settings, + return { + ...field, + key: field.key, + value: '', }; + } - if (field.condition) { - const evaluated = evaluate(field.condition, evaluationContext); + const evaluationContext = { + ...formData, + ...accountData?.setupInformation, + external_trigger_token: hubData?.external_trigger_token, + webhooks_url: hubData?.webhooks_url, + events_encoded_context: hubData?.events_encoded_context, + hub_settings: connectorData.hub_settings, + }; - const shouldShow = evaluated != null && evaluated !== 'false'; + if (field.condition) { + const evaluated = evaluate(field.condition, evaluationContext); - if (!shouldShow) { - return; - } - } + const shouldShow = evaluated != null && evaluated !== 'false'; - const valueToEvaluate = setupValue !== undefined ? setupValue : field.value; - - if (!valueToEvaluate) { - return { - ...field, - key: field.key, - }; + if (!shouldShow) { + return; } - let evaluatedValue = evaluate( - valueToEvaluate?.toString(), - evaluationContext, - ); + } - if (typeof evaluatedValue === 'object' && evaluatedValue !== null) { - evaluatedValue = JSON.stringify(evaluatedValue); - } + const valueToEvaluate = setupValue !== undefined ? setupValue : field.value; + if (!valueToEvaluate) { return { ...field, key: field.key, - value: evaluatedValue as string | number | undefined, }; - }) - .filter((value) => value != null); + } + let evaluatedValue = evaluate(valueToEvaluate?.toString(), evaluationContext); + + if (typeof evaluatedValue === 'object' && evaluatedValue !== null) { + evaluatedValue = JSON.stringify(evaluatedValue); + } + + return { + ...field, + key: field.key, + value: evaluatedValue as string | number | undefined, + }; + }) + .filter((value) => value != null); return { fields: fieldsWithPrefilledValues, + notices: connectorData.config.configNotices ?? [], guide: { supportLink: connectorData.config.support?.link, description: connectorData.config.support?.description ?? '', @@ -507,6 +508,7 @@ export const useIntegrationPicker = ({ return { fields: fieldsWithPrefilledValues, + notices: [], guide: authConfigForEnvironment?.guide, }; }, [connectorData, selectedIntegration, accountData, formData, hubData]); @@ -901,6 +903,7 @@ export const useIntegrationPicker = ({ connectorData, selectedIntegration, fields, + notices, guide, // State diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 08f908c..095c342 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -18,8 +18,7 @@ export interface HubData { } export interface ConnectorConfigField { - type?: 'text' | 'password' | 'number' | 'select' | 'text_area' | 'alert'; - alertType?: 'warning' | 'info'; + type?: 'text' | 'password' | 'number' | 'select' | 'text_area'; label: string; key: string; required: boolean; @@ -66,12 +65,21 @@ export interface LegacyConnectorConfig { }; } +export interface AuthenticationNotice { + key: string; + type: 'warning' | 'info'; + description: string; + position?: 'top' | 'bottom'; + anchor?: string; +} + export interface FalconConnectorConfig { key: string; name: string; type: 'oauth2' | 'custom'; grantType?: 'authorization_code' | 'client_credentials'; configFields: Array; + configNotices?: Array; assets?: { icon: string; }; @@ -101,7 +109,7 @@ export function isLegacyConnectorConfig(config: ConnectorConfig): config is Lega } export function isFalconConnectorConfig(config: ConnectorConfig): config is FalconConnectorConfig { - return 'configFields' in config && !('authentication' in config); + return !('authentication' in config); } export interface AccountData { diff --git a/src/modules/integration-picker/utils/partitionNotices.ts b/src/modules/integration-picker/utils/partitionNotices.ts new file mode 100644 index 0000000..7c73aba --- /dev/null +++ b/src/modules/integration-picker/utils/partitionNotices.ts @@ -0,0 +1,20 @@ +import type { AuthenticationNotice } from '../types'; + +export function partitionNotices(notices: AuthenticationNotice[], fieldKeys: string[] = []) { + const firstKey = fieldKeys[0]; + const lastKey = fieldKeys[fieldKeys.length - 1]; + + const noticesBefore = (fieldKey: string) => + notices.filter((n) => { + if (n.anchor) return n.anchor === fieldKey; + return (!n.position || n.position === 'top') && fieldKey === firstKey; + }); + + const noticesAfter = (fieldKey: string) => + notices.filter((n) => { + if (n.anchor) return false; + return n.position === 'bottom' && fieldKey === lastKey; + }); + + return { noticesBefore, noticesAfter }; +} diff --git a/src/modules/integration-picker/utils/zodSchema.ts b/src/modules/integration-picker/utils/zodSchema.ts index 5c61c25..6e399da 100644 --- a/src/modules/integration-picker/utils/zodSchema.ts +++ b/src/modules/integration-picker/utils/zodSchema.ts @@ -55,7 +55,6 @@ export function createFormSchema(fields: ConnectorConfigField[]) { const schemaShape: Record = {}; for (const field of fields) { - if (field.type === 'alert') continue; schemaShape[field.key] = createFieldSchema(field); } From 9dbb921bfbf59006963b24d44d8c500dac467b15 Mon Sep 17 00:00:00 2001 From: Sourabh Sen Date: Wed, 13 May 2026 16:26:52 +0530 Subject: [PATCH 3/5] add hasAnchor function with union type --- src/modules/integration-picker/types.ts | 21 +++++++++++++++---- .../utils/partitionNotices.ts | 5 +++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 095c342..0aa3d8a 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -65,12 +65,25 @@ export interface LegacyConnectorConfig { }; } -export interface AuthenticationNotice { +type AuthenticationNoticeTop = { key: string; type: 'warning' | 'info'; description: string; - position?: 'top' | 'bottom'; + position?: 'top'; anchor?: string; +}; + +type AuthenticationNoticeBottom = { + key: string; + type: 'warning' | 'info'; + description: string; + position: 'bottom'; +}; + +export type AuthenticationNotice = AuthenticationNoticeTop | AuthenticationNoticeBottom; + +export function hasAnchor(notice: AuthenticationNotice): notice is AuthenticationNoticeTop & { anchor: string } { + return 'anchor' in notice && typeof notice.anchor === 'string' && notice.anchor.trim().length > 0; } export interface FalconConnectorConfig { @@ -78,7 +91,7 @@ export interface FalconConnectorConfig { name: string; type: 'oauth2' | 'custom'; grantType?: 'authorization_code' | 'client_credentials'; - configFields: Array; + configFields?: Array; configNotices?: Array; assets?: { icon: string; @@ -109,7 +122,7 @@ export function isLegacyConnectorConfig(config: ConnectorConfig): config is Lega } export function isFalconConnectorConfig(config: ConnectorConfig): config is FalconConnectorConfig { - return !('authentication' in config); + return ('configFields' in config || 'configNotices' in config) && !('authentication' in config); } export interface AccountData { diff --git a/src/modules/integration-picker/utils/partitionNotices.ts b/src/modules/integration-picker/utils/partitionNotices.ts index 7c73aba..9c0ed6a 100644 --- a/src/modules/integration-picker/utils/partitionNotices.ts +++ b/src/modules/integration-picker/utils/partitionNotices.ts @@ -1,3 +1,4 @@ +import { hasAnchor } from '../types'; import type { AuthenticationNotice } from '../types'; export function partitionNotices(notices: AuthenticationNotice[], fieldKeys: string[] = []) { @@ -6,13 +7,13 @@ export function partitionNotices(notices: AuthenticationNotice[], fieldKeys: str const noticesBefore = (fieldKey: string) => notices.filter((n) => { - if (n.anchor) return n.anchor === fieldKey; + if (hasAnchor(n)) return n.anchor === fieldKey; return (!n.position || n.position === 'top') && fieldKey === firstKey; }); const noticesAfter = (fieldKey: string) => notices.filter((n) => { - if (n.anchor) return false; + if (hasAnchor(n)) return false; return n.position === 'bottom' && fieldKey === lastKey; }); From 6e14ed0126573e0c567b91034ac42e78f3e3c529 Mon Sep 17 00:00:00 2001 From: Sourabh Sen Date: Wed, 13 May 2026 16:27:15 +0530 Subject: [PATCH 4/5] add hasAnchor function updates --- src/modules/integration-picker/types.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/integration-picker/types.ts b/src/modules/integration-picker/types.ts index 0aa3d8a..bf7543c 100644 --- a/src/modules/integration-picker/types.ts +++ b/src/modules/integration-picker/types.ts @@ -82,8 +82,12 @@ type AuthenticationNoticeBottom = { export type AuthenticationNotice = AuthenticationNoticeTop | AuthenticationNoticeBottom; -export function hasAnchor(notice: AuthenticationNotice): notice is AuthenticationNoticeTop & { anchor: string } { - return 'anchor' in notice && typeof notice.anchor === 'string' && notice.anchor.trim().length > 0; +export function hasAnchor( + notice: AuthenticationNotice, +): notice is AuthenticationNoticeTop & { anchor: string } { + return ( + 'anchor' in notice && typeof notice.anchor === 'string' && notice.anchor.trim().length > 0 + ); } export interface FalconConnectorConfig { From 2716390a01ee9e83e496bd2fe0440722742de843 Mon Sep 17 00:00:00 2001 From: Sourabh Sen Date: Thu, 14 May 2026 12:40:59 +0530 Subject: [PATCH 5/5] apply fixes as per the suggestions --- .../components/IntegrationFields.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/modules/integration-picker/components/IntegrationFields.tsx b/src/modules/integration-picker/components/IntegrationFields.tsx index b7c0967..892b727 100644 --- a/src/modules/integration-picker/components/IntegrationFields.tsx +++ b/src/modules/integration-picker/components/IntegrationFields.tsx @@ -15,7 +15,7 @@ import { TextArea, Typography, } from '@stackone/malachite'; -import { Fragment, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { FieldErrors, UseFormSetValue } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import useDeepCompareEffect from 'use-deep-compare-effect'; @@ -321,7 +321,7 @@ export const IntegrationForm: React.FC = ({ onValidationChange?.(isValid); }, [isValid, onValidationChange]); - if (fields.length === 0) { + if (displayedFields.length === 0) { return ; } @@ -339,13 +339,17 @@ export const IntegrationForm: React.FC = ({ typeof field.key === 'object' ? JSON.stringify(field.key) : String(field.key); + const hasNotices = + noticesBefore(key).length > 0 || noticesAfter(key).length > 0; return (