|
| 1 | +/** |
| 2 | + * Collapsible card shared by the MCP Read and Write views. |
| 3 | + */ |
| 4 | + |
| 5 | +import { Button } from '@wordpress/components'; |
| 6 | +import { Fragment, useCallback, useState } from '@wordpress/element'; |
| 7 | +import { __ } from '@wordpress/i18n'; |
| 8 | +import { chevronDown, chevronUp } from '@wordpress/icons'; |
| 9 | +import { Card, Stack, SwitchControl, Text } from '@wordpress/ui'; |
| 10 | +import { groupToolsBySubCategory } from './groups'; |
| 11 | + |
| 12 | +const LABEL_SHOW_OPERATIONS = __( 'Show operations', 'jetpack' ); |
| 13 | +const LABEL_HIDE_OPERATIONS = __( 'Hide operations', 'jetpack' ); |
| 14 | + |
| 15 | +/** |
| 16 | + * A single tool toggle row. |
| 17 | + * |
| 18 | + * @param {object} props - Component props. |
| 19 | + * @param {string} props.toolId - Tool identifier. |
| 20 | + * @param {object} props.tool - Tool descriptor from the API. |
| 21 | + * @param {Set} props.savingToolIds - Set of toolIds currently being saved. |
| 22 | + * @param {Function} props.onToggle - Called with (toolId, enabled). |
| 23 | + * @return {object} Component markup. |
| 24 | + */ |
| 25 | +function ToolToggle( { toolId, tool, savingToolIds, onToggle } ) { |
| 26 | + const handleChange = useCallback( checked => onToggle( toolId, checked ), [ toolId, onToggle ] ); |
| 27 | + return ( |
| 28 | + <SwitchControl |
| 29 | + className="jetpack-ai-mcp__tool-switch" |
| 30 | + checked={ tool.enabled } |
| 31 | + disabled={ savingToolIds.has( toolId ) } |
| 32 | + label={ tool.title } |
| 33 | + description={ tool.description } |
| 34 | + onCheckedChange={ handleChange } |
| 35 | + /> |
| 36 | + ); |
| 37 | +} |
| 38 | + |
| 39 | +/** |
| 40 | + * Tool toggles split into sub-category sections. |
| 41 | + * |
| 42 | + * @param {object} props - Component props. |
| 43 | + * @param {Array} props.tools - Tool entries, each as [toolId, ability]. |
| 44 | + * @param {Set} props.savingToolIds - Set of toolIds currently being saved. |
| 45 | + * @param {Function} props.onToolChange - Called with (toolId, enabled). |
| 46 | + * @return {object} Component markup. |
| 47 | + */ |
| 48 | +export function McpToolSections( { tools, savingToolIds, onToolChange } ) { |
| 49 | + return groupToolsBySubCategory( tools ).map( ( { subCategory, tools: subTools }, subIndex ) => ( |
| 50 | + <Fragment key={ subCategory ?? '__ungrouped__' }> |
| 51 | + { subIndex > 0 && <div className="jetpack-ai-mcp__sub-divider" /> } |
| 52 | + <div |
| 53 | + className={ |
| 54 | + subIndex === 0 ? 'jetpack-ai-mcp__group-body' : 'jetpack-ai-mcp__sub-group-body' |
| 55 | + } |
| 56 | + > |
| 57 | + <Stack direction="column" gap="md"> |
| 58 | + { subTools.map( ( [ toolId, tool ] ) => ( |
| 59 | + <ToolToggle |
| 60 | + key={ toolId } |
| 61 | + toolId={ toolId } |
| 62 | + tool={ tool } |
| 63 | + savingToolIds={ savingToolIds } |
| 64 | + onToggle={ onToolChange } |
| 65 | + /> |
| 66 | + ) ) } |
| 67 | + </Stack> |
| 68 | + </div> |
| 69 | + </Fragment> |
| 70 | + ) ); |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Collapsible card with a group-wide "Enable all" toggle. |
| 75 | + * |
| 76 | + * @param {object} props - Component props. |
| 77 | + * @param {string} [props.id] - DOM id for the card. |
| 78 | + * @param {string} props.label - Group title. |
| 79 | + * @param {string} [props.description] - Muted text under the title. |
| 80 | + * @param {boolean} props.allEnabled - Whether every tool in the group is on. |
| 81 | + * @param {boolean} props.isSaving - Disables the toggle while a save is in flight. |
| 82 | + * @param {boolean} [props.forceOpen] - Keep the card open regardless of the chevron. |
| 83 | + * @param {*} [props.group] - Opaque value handed back to onEnableAll. |
| 84 | + * @param {Function} props.onEnableAll - Called with (enabled, group). |
| 85 | + * @param {object} props.children - Card body, rendered while open. |
| 86 | + * @return {object} Component markup. |
| 87 | + */ |
| 88 | +export default function McpGroupCard( { |
| 89 | + id, |
| 90 | + label, |
| 91 | + description, |
| 92 | + allEnabled, |
| 93 | + isSaving, |
| 94 | + forceOpen = false, |
| 95 | + group, |
| 96 | + onEnableAll, |
| 97 | + children, |
| 98 | +} ) { |
| 99 | + const [ isToggledOpen, setIsToggledOpen ] = useState( false ); |
| 100 | + const isOpen = forceOpen || isToggledOpen; |
| 101 | + const handleEnableAll = useCallback( |
| 102 | + enabled => onEnableAll( enabled, group ), |
| 103 | + [ onEnableAll, group ] |
| 104 | + ); |
| 105 | + const handleChevron = useCallback( () => setIsToggledOpen( open => ! open ), [] ); |
| 106 | + |
| 107 | + return ( |
| 108 | + <Card.Root id={ id }> |
| 109 | + <Card.Content className="jetpack-ai-mcp__card-content"> |
| 110 | + <div className="jetpack-ai-mcp__group-header"> |
| 111 | + <Stack direction="column" gap="xs" className="jetpack-ai-mcp__group-info"> |
| 112 | + <Text className="jetpack-ai-mcp__group-title">{ label }</Text> |
| 113 | + { description && ( |
| 114 | + <Text variant="body-sm" className="jetpack-ai-mcp__muted"> |
| 115 | + { description } |
| 116 | + </Text> |
| 117 | + ) } |
| 118 | + </Stack> |
| 119 | + <div className="jetpack-ai-mcp__group-toggle"> |
| 120 | + <SwitchControl |
| 121 | + checked={ allEnabled } |
| 122 | + disabled={ isSaving } |
| 123 | + label={ __( 'Enable all', 'jetpack' ) } |
| 124 | + onCheckedChange={ handleEnableAll } |
| 125 | + /> |
| 126 | + </div> |
| 127 | + <Button |
| 128 | + className="jetpack-ai-mcp__group-chevron" |
| 129 | + icon={ isOpen ? chevronUp : chevronDown } |
| 130 | + label={ isOpen ? LABEL_HIDE_OPERATIONS : LABEL_SHOW_OPERATIONS } |
| 131 | + aria-expanded={ isOpen } |
| 132 | + disabled={ forceOpen } |
| 133 | + onClick={ handleChevron } |
| 134 | + /> |
| 135 | + </div> |
| 136 | + { isOpen && children } |
| 137 | + </Card.Content> |
| 138 | + </Card.Root> |
| 139 | + ); |
| 140 | +} |
0 commit comments