diff --git a/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts b/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts index 616e0db6d6f..777b83a05a7 100644 --- a/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts +++ b/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsInterfaces.ts @@ -54,6 +54,7 @@ export interface DesignerOptionsState { enableNestedAgentLoops?: boolean; // allow agent loops to be added inside regular loops (requires bundle version >= 1.115.0) disableMcpClientTools?: boolean; // hide MCP client tools from browse panel disableNativeMcpClientTools?: boolean; // hide native (built-in) MCP client tools tab from browse panel + hiddenBrowseCategories?: string[]; // hide specific categories from browse panel by category key (e.g., ['aiAgent', 'humanInTheLoop', 'favorites']) }; nodeSelectAdditionalCallback?: (nodeId: string) => any; panelTabHideKeys?: PANEL_TAB_NAMES[]; diff --git a/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsSelectors.ts b/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsSelectors.ts index 592bf2354e7..28c403fc62c 100644 --- a/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsSelectors.ts +++ b/libs/designer-v2/src/lib/core/state/designerOptions/designerOptionsSelectors.ts @@ -7,6 +7,8 @@ import { equals } from '@microsoft/logic-apps-shared'; import { getSupportedChannels } from '../../utils/agent'; import constants from '../../../common/constants'; +const EMPTY_ARRAY: string[] = []; + export const useReadOnly = () => { return useSelector((state: RootState) => state.designerOptions.readOnly); }; @@ -70,6 +72,10 @@ export const useDisableNativeMcpClientTools = () => { return useSelector((state: RootState) => state.designerOptions.hostOptions?.disableNativeMcpClientTools ?? false); }; +export const useHiddenBrowseCategories = () => { + return useSelector((state: RootState) => state.designerOptions.hostOptions?.hiddenBrowseCategories ?? EMPTY_ARRAY); +}; + export const useAreDesignerOptionsInitialized = () => { return useSelector((state: RootState) => state.designerOptions?.designerOptionsInitialized ?? false); }; diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/browseView.spec.tsx b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/browseView.spec.tsx index 3361e7fac77..878417a95fa 100644 --- a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/browseView.spec.tsx +++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/browseView.spec.tsx @@ -317,7 +317,7 @@ describe('BrowseView', () => { render(, { wrapper: createWrapper() }); // getActionCategories should be called with allowAgents=true - expect(mockGetActionCategories).toHaveBeenCalledWith(true, false, false); + expect(mockGetActionCategories).toHaveBeenCalledWith(true, false, false, []); }); test('should pass allowAgents as false when graphId is not root', () => { @@ -330,7 +330,7 @@ describe('BrowseView', () => { render(, { wrapper: createWrapper() }); // getActionCategories should be called with allowAgents=false - expect(mockGetActionCategories).toHaveBeenCalledWith(false, false, false); + expect(mockGetActionCategories).toHaveBeenCalledWith(false, false, false, []); }); test('should pass isAddingAgentTool to getActionCategories', () => { @@ -338,7 +338,7 @@ describe('BrowseView', () => { render(, { wrapper: createWrapper() }); - expect(mockGetActionCategories).toHaveBeenCalledWith(true, true, false); + expect(mockGetActionCategories).toHaveBeenCalledWith(true, true, false, []); }); }); }); diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/helper.spec.ts b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/helper.spec.ts index bf0325d3f02..3adca02c30d 100644 --- a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/helper.spec.ts +++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/__test__/helper.spec.ts @@ -166,4 +166,91 @@ describe('browse helper', () => { expect(humanInTheLoop?.connectorFilters?.name).toContain('teams'); }); }); + + describe('hiddenBrowseCategories', () => { + describe('action categories', () => { + test('should hide aiAgent when included in hiddenCategories', () => { + const categories = getActionCategories(true, false, false, ['aiAgent']); + const aiAgent = categories.find((c) => c.key === 'aiAgent'); + expect(aiAgent?.visible).toBe(false); + }); + + test('should hide humanInTheLoop when included in hiddenCategories', () => { + const categories = getActionCategories(false, false, false, ['humanInTheLoop']); + const humanInTheLoop = categories.find((c) => c.key === 'humanInTheLoop'); + expect(humanInTheLoop?.visible).toBe(false); + }); + + test('should hide multiple categories', () => { + const categories = getActionCategories(true, false, false, ['aiAgent', 'humanInTheLoop', 'favorites']); + expect(categories.find((c) => c.key === 'aiAgent')?.visible).toBe(false); + expect(categories.find((c) => c.key === 'humanInTheLoop')?.visible).toBe(false); + expect(categories.find((c) => c.key === 'favorites')?.visible).toBe(false); + }); + + test('should not affect non-hidden categories', () => { + const categories = getActionCategories(true, false, false, ['aiAgent']); + const actionInApp = categories.find((c) => c.key === 'actionInApp'); + expect(actionInApp?.visible).toBeUndefined(); // undefined = visible by default + }); + + test('should work with empty array', () => { + const categories = getActionCategories(true, false, false, []); + expect(categories.find((c) => c.key === 'aiAgent')?.visible).toBe(true); + }); + + test('should work with undefined', () => { + const categories = getActionCategories(true, false, false, undefined); + expect(categories.find((c) => c.key === 'aiAgent')?.visible).toBe(true); + }); + + test('should respect allowAgents flag even when not in hiddenCategories', () => { + const categoriesWithAgents = getActionCategories(true, false, false, []); + const categoriesWithoutAgents = getActionCategories(false, false, false, []); + + expect(categoriesWithAgents.find((c) => c.key === 'aiAgent')?.visible).toBe(true); + expect(categoriesWithoutAgents.find((c) => c.key === 'aiAgent')?.visible).toBe(false); + }); + + test('should hide aiAgent via hiddenCategories even when allowAgents is true', () => { + const categories = getActionCategories(true, false, false, ['aiAgent']); + expect(categories.find((c) => c.key === 'aiAgent')?.visible).toBe(false); + }); + }); + + describe('trigger categories', () => { + test('should hide manual trigger when included in hiddenCategories', () => { + const categories = getTriggerCategories(['manual']); + expect(categories.find((c) => c.key === 'manual')?.visible).toBe(false); + }); + + test('should hide schedule trigger when included in hiddenCategories', () => { + const categories = getTriggerCategories(['schedule']); + expect(categories.find((c) => c.key === 'schedule')?.visible).toBe(false); + }); + + test('should hide multiple trigger categories', () => { + const categories = getTriggerCategories(['manual', 'schedule', 'appEvent']); + expect(categories.find((c) => c.key === 'manual')?.visible).toBe(false); + expect(categories.find((c) => c.key === 'schedule')?.visible).toBe(false); + expect(categories.find((c) => c.key === 'appEvent')?.visible).toBe(false); + }); + + test('should not affect non-hidden trigger categories', () => { + const categories = getTriggerCategories(['manual']); + const schedule = categories.find((c) => c.key === 'schedule'); + expect(schedule?.visible).toBeUndefined(); // undefined = visible by default + }); + + test('should work with empty array for triggers', () => { + const categories = getTriggerCategories([]); + expect(categories.find((c) => c.key === 'manual')?.visible).toBeUndefined(); + }); + + test('should work with undefined for triggers', () => { + const categories = getTriggerCategories(undefined); + expect(categories.find((c) => c.key === 'manual')?.visible).toBeUndefined(); + }); + }); + }); }); diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/browseView.tsx b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/browseView.tsx index 58f9b8b9830..c412e318cf1 100644 --- a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/browseView.tsx +++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/browseView.tsx @@ -17,7 +17,7 @@ import type { AppDispatch } from '../../../../core'; import { equals, type DiscoveryOperation, type DiscoveryResultTypes } from '@microsoft/logic-apps-shared'; import { getNodeId } from '../helpers'; import { getTriggerCategories, getActionCategories, BrowseCategoryType } from './helper'; -import { useDisableMcpClientTools } from '../../../../core/state/designerOptions/designerOptionsSelectors'; +import { useDisableMcpClientTools, useHiddenBrowseCategories } from '../../../../core/state/designerOptions/designerOptionsSelectors'; interface BrowseViewProps { isTrigger?: boolean; @@ -35,8 +35,11 @@ export const BrowseView = ({ isTrigger = false, onOperationClick }: BrowseViewPr const allowAgents = equals(relationshipIds.graphId, 'root'); const isAddingAgentTool = useIsAddingAgentTool(); const disableMcpClientTools = useDisableMcpClientTools(); + const hiddenCategories = useHiddenBrowseCategories(); - const categories = isTrigger ? getTriggerCategories() : getActionCategories(allowAgents, isAddingAgentTool, disableMcpClientTools); + const categories = isTrigger + ? getTriggerCategories(hiddenCategories) + : getActionCategories(allowAgents, isAddingAgentTool, disableMcpClientTools, hiddenCategories); const addTriggerOperation = useCallback( (operation: DiscoveryOperation) => { diff --git a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/helper.ts b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/helper.ts index e891c71f1da..3dc1463635b 100644 --- a/libs/designer-v2/src/lib/ui/panel/recommendation/browse/helper.ts +++ b/libs/designer-v2/src/lib/ui/panel/recommendation/browse/helper.ts @@ -48,10 +48,15 @@ export interface BrowseCategoryConfig { connectorFilters?: ConnectorFilterTypes; } -export const getTriggerCategories = (): BrowseCategoryConfig[] => { +/** + * Get trigger categories for the browse panel. + * @param hiddenCategories - Array of category keys to hide. Available keys: 'manual', 'schedule', 'appEvent', 'azure', 'workflowExecution', 'chatMessage', 'evaluation', 'otherWays' + * @returns Array of trigger category configurations + */ +export const getTriggerCategories = (hiddenCategories?: string[]): BrowseCategoryConfig[] => { const intl = getIntl(); - return [ + const categories = [ { key: 'manual', text: intl.formatMessage({ @@ -183,16 +188,28 @@ export const getTriggerCategories = (): BrowseCategoryConfig[] => { type: BrowseCategoryType.BROWSE, }, ]; + + // Apply hidden categories filter + return categories.map((category) => (hiddenCategories?.includes(category.key) ? { ...category, visible: false } : category)); }; +/** + * Get action categories for the browse panel. + * @param allowAgents - Whether to show AI agent category (based on graph root) + * @param isAddingAgentTool - Whether currently adding an agent tool + * @param disableMcpClientTools - Whether to disable MCP client tools category + * @param hiddenCategories - Array of category keys to hide. Available keys: 'favorites', 'mcpServers', 'aiAgent', 'actionInApp', 'dataTransformation', 'simpleOperations', 'humanInTheLoop' + * @returns Array of action category configurations + */ export const getActionCategories = ( allowAgents?: boolean, isAddingAgentTool?: boolean, - disableMcpClientTools?: boolean + disableMcpClientTools?: boolean, + hiddenCategories?: string[] ): BrowseCategoryConfig[] => { const intl = getIntl(); - return [ + const categories = [ { key: 'favorites', text: intl.formatMessage({ @@ -337,4 +354,7 @@ export const getActionCategories = ( }, }, ]; + + // Apply hidden categories filter + return categories.map((category) => (hiddenCategories?.includes(category.key) ? { ...category, visible: false } : category)); };