1- import React , { useEffect , useRef , useState } from 'react'
2- import { Button , Col , FormControl , Row } from 'react-bootstrap'
1+ import React , { useEffect , useMemo , useRef , useState } from 'react'
2+ import { Button , Dropdown , FormControl } from 'react-bootstrap'
33
44import { useSoraDevtoolsStore } from '@/app/store'
55import { API_TEMPLATES } from '@/constants'
@@ -8,6 +8,9 @@ import { JSONInputField } from '@/components/DevtoolsPane/JSONInputField.tsx'
88
99import { JsonTree } from './JsonTree.tsx'
1010
11+ type ApiTemplate = ( typeof API_TEMPLATES ) [ number ]
12+ type ApiTemplateGroups = Record < string , ApiTemplate [ ] >
13+
1114const ClearButton = React . memo ( ( ) => {
1215 const onClick = ( ) : void => {
1316 useSoraDevtoolsStore . getState ( ) . clearApiObjects ( )
@@ -25,8 +28,8 @@ type ApiFormProps = {
2528 selectedMethod : string
2629 params : string
2730 setParams : ( params : string ) => void
28- setShowModal : ( show : boolean ) => void
29- buttonRef : React . RefObject < HTMLButtonElement | null >
31+ templateGroups : ApiTemplateGroups
32+ onSelectMethod : ( method : string , methodParams ?: Record < string , unknown > | unknown [ ] ) => void
3033}
3134
3235const ApiForm : React . FC < ApiFormProps > = ( {
@@ -35,8 +38,8 @@ const ApiForm: React.FC<ApiFormProps> = ({
3538 selectedMethod,
3639 params,
3740 setParams,
38- setShowModal ,
39- buttonRef ,
41+ templateGroups ,
42+ onSelectMethod ,
4043} ) => {
4144 const urlRef = useRef < HTMLInputElement > ( null )
4245 const timeoutRef = useRef < HTMLInputElement > ( null )
@@ -240,14 +243,37 @@ const ApiForm: React.FC<ApiFormProps> = ({
240243 < div className = "mb-1" style = { { color : '#fff' } } >
241244 < strong > method:</ strong >
242245 </ div >
243- < Button
244- ref = { buttonRef }
245- variant = "secondary"
246- onClick = { ( ) => setShowModal ( true ) }
247- style = { { width : '100%' , fontSize : '1rem' , fontWeight : 'bold' } }
248- >
249- { selectedMethod || 'Select method' }
250- </ Button >
246+ < Dropdown className = "w-100" >
247+ < Dropdown . Toggle
248+ id = "debug-api-method-dropdown"
249+ variant = "secondary"
250+ style = { { width : '100%' , fontSize : '1rem' , fontWeight : 'bold' , textAlign : 'left' } }
251+ >
252+ { selectedMethod || 'Select method' }
253+ </ Dropdown . Toggle >
254+ < Dropdown . Menu
255+ className = "w-100"
256+ style = { { maxHeight : '420px' , overflowY : 'auto' , minWidth : '480px' , width : '100%' } }
257+ data-testid = "debug-api-method-menu"
258+ >
259+ { Object . entries ( templateGroups ) . map ( ( [ groupName , templates ] , index , entries ) => (
260+ < React . Fragment key = { groupName } >
261+ < Dropdown . Header > { groupName } </ Dropdown . Header >
262+ { templates . map ( ( template ) => (
263+ < Dropdown . Item
264+ key = { template . method }
265+ active = { selectedMethod === template . method }
266+ onClick = { ( ) => onSelectMethod ( template . method , template . params ) }
267+ style = { { fontWeight : 'bold' } }
268+ >
269+ { template . method . replace ( 'Sora_' , '' ) }
270+ </ Dropdown . Item >
271+ ) ) }
272+ { index < entries . length - 1 && < Dropdown . Divider /> }
273+ </ React . Fragment >
274+ ) ) }
275+ </ Dropdown . Menu >
276+ </ Dropdown >
251277 </ div >
252278
253279 < div style = { { width : '150px' } } >
@@ -552,26 +578,14 @@ export const Api: React.FC = () => {
552578 }
553579 const [ selectedMethod , setSelectedMethod ] = useState ( '' )
554580 const [ params , setParams ] = useState ( '' )
555- const [ showModal , setShowModal ] = useState ( false )
556- const buttonRef = useRef < HTMLButtonElement > ( null )
557- const [ modalTop , setModalTop ] = useState ( 0 )
558- const [ modalLeft , setModalLeft ] = useState ( 0 )
559- const [ modalWidth , setModalWidth ] = useState ( 0 )
560-
561- // ボタンの位置が変わったときにモーダルの位置を更新
562- useEffect ( ( ) => {
563- if ( showModal && buttonRef . current ) {
564- const rect = buttonRef . current . getBoundingClientRect ( )
565- // API タブページの幅を取得するため、親要素を探す
566- const container = buttonRef . current . closest ( '[style*="position: relative"]' )
567- if ( container ) {
568- const containerRect = container . getBoundingClientRect ( )
569- setModalTop ( rect . bottom + 4 )
570- setModalLeft ( containerRect . left )
571- setModalWidth ( containerRect . width )
572- }
573- }
574- } , [ showModal ] )
581+ const templateGroups = useMemo ( ( ) => {
582+ return API_TEMPLATES . reduce ( ( acc : ApiTemplateGroups , template ) => {
583+ const group = template . group || 'Other'
584+ if ( ! acc [ group ] ) acc [ group ] = [ ]
585+ acc [ group ] . push ( template )
586+ return acc
587+ } , { } )
588+ } , [ ] )
575589
576590 const handleReuse = ( apiObject : ApiObject ) : void => {
577591 setUrl ( apiObject . url )
@@ -595,113 +609,18 @@ export const Api: React.FC = () => {
595609 } else {
596610 setParams ( '' )
597611 }
598- setShowModal ( false )
599612 }
600613
601614 return (
602615 < div style = { { position : 'relative' , height : '100%' , display : 'flex' , flexDirection : 'column' } } >
603- { showModal && (
604- < div
605- style = { {
606- position : 'fixed' ,
607- top : 0 ,
608- left : 0 ,
609- right : 0 ,
610- bottom : 0 ,
611- backgroundColor : 'rgba(0, 0, 0, 0.5)' ,
612- zIndex : 998 ,
613- } }
614- onClick = { ( ) => setShowModal ( false ) }
615- onKeyDown = { ( e ) => {
616- if ( e . key === 'Escape' ) {
617- setShowModal ( false )
618- }
619- } }
620- />
621- ) }
622- { showModal && (
623- < div
624- style = { {
625- position : 'fixed' ,
626- top : `${ modalTop } px` ,
627- left : `${ modalLeft } px` ,
628- width : `${ modalWidth } px` ,
629- backgroundColor : '#1a1a1a' ,
630- border : '1px solid #444' ,
631- borderRadius : '8px' ,
632- maxHeight : `calc(100vh - ${ modalTop } px - 20px)` ,
633- overflowY : 'auto' ,
634- zIndex : 1000 ,
635- boxShadow : '0 4px 6px rgba(0, 0, 0, 0.3)' ,
636- padding : '20px' ,
637- } }
638- >
639- < Button
640- variant = "outline-light"
641- size = "sm"
642- onClick = { ( ) => setShowModal ( false ) }
643- style = { {
644- position : 'absolute' ,
645- top : '10px' ,
646- right : '10px' ,
647- zIndex : 1 ,
648- } }
649- >
650- ×
651- </ Button >
652- { ( ( ) => {
653- type TemplateType = ( typeof API_TEMPLATES ) [ number ]
654- const groups = API_TEMPLATES . reduce ( ( acc : Record < string , TemplateType [ ] > , template ) => {
655- const group = template . group || 'Other'
656- if ( ! acc [ group ] ) acc [ group ] = [ ]
657- acc [ group ] . push ( template )
658- return acc
659- } , { } )
660-
661- return Object . entries ( groups ) . map ( ( [ groupName , templates ] ) => (
662- < div key = { groupName } className = "mb-4" >
663- < div
664- style = { {
665- color : '#fff' ,
666- fontWeight : 'bold' ,
667- marginBottom : '12px' ,
668- fontSize : '1.1rem' ,
669- } }
670- >
671- { groupName }
672- </ div >
673- < Row >
674- { templates . map ( ( template ) => (
675- < Col key = { template . method } xs = { 6 } className = "mb-2" >
676- < Button
677- variant = { selectedMethod === template . method ? 'primary' : 'secondary' }
678- size = "sm"
679- style = { {
680- width : '100%' ,
681- fontSize : '1.1rem' ,
682- padding : '12px' ,
683- fontWeight : 'bold' ,
684- } }
685- onClick = { ( ) => handleMethodSelect ( template . method , template . params ) }
686- >
687- { template . method . replace ( 'Sora_' , '' ) }
688- </ Button >
689- </ Col >
690- ) ) }
691- </ Row >
692- </ div >
693- ) )
694- } ) ( ) }
695- </ div >
696- ) }
697616 < ApiForm
698617 url = { url }
699618 setUrl = { setUrl }
700619 selectedMethod = { selectedMethod }
701620 params = { params }
702621 setParams = { setParams }
703- setShowModal = { setShowModal }
704- buttonRef = { buttonRef }
622+ templateGroups = { templateGroups }
623+ onSelectMethod = { handleMethodSelect }
705624 />
706625 { apiObjects . length > 0 && (
707626 < >
0 commit comments