11import { useState } from 'react' ;
22import { ACTION_TYPES , CLICK_TYPES , buildActionString , describeAction , parseAction } from '../../editor/actions' ;
3+ import { Modal } from '../Modal' ;
34
45interface ActionEditorProps {
56 actions : string [ ] ;
67 onChange : ( actions : string [ ] ) => void ;
78}
89
9- /** Click actions of one item, edited as a list rather than as raw strings . */
10+ /** Click actions of one item, edited in a dialog as the legacy editor did . */
1011export function ActionEditor ( { actions, onChange } : ActionEditorProps ) {
1112 const [ editing , setEditing ] = useState < number | null > ( null ) ;
13+ const [ dragged , setDragged ] = useState < number | null > ( null ) ;
1214
13- const replace = ( index : number , action : string ) : void => {
14- onChange ( actions . map ( ( existing , position ) => ( position === index ? action : existing ) ) ) ;
15- setEditing ( null ) ;
16- } ;
15+ const close = ( ) : void => setEditing ( null ) ;
1716
1817 return (
19- < section className = "item-editor-section" >
20- < header className = "section-heading" >
18+ < >
19+ < div className = "section-heading" >
2120 < span > Actions</ span >
2221 < button
2322 type = "button"
23+ className = "btn btn-secondary btn-sm"
2424 onClick = { ( ) => {
2525 onChange ( [ ...actions , '[LEFT_CLICK] CLOSE' ] ) ;
2626 setEditing ( actions . length ) ;
2727 } }
28- className = "btn btn-secondary btn-sm"
2928 >
30- Add
29+ + Add
3130 </ button >
32- </ header >
31+ </ div >
3332
34- { actions . length === 0 && < p className = "empty-note" > This item does nothing when clicked.</ p > }
33+ < div className = "actions-list" >
34+ { actions . length === 0 && (
35+ < div className = "actions-empty" >
36+ < span > This item does nothing when clicked.</ span >
37+ </ div >
38+ ) }
3539
36- < ul >
3740 { actions . map ( ( action , index ) => {
3841 const parsed = parseAction ( action ) ;
3942
4043 return (
41- < li key = { `${ index } -${ action } ` } className = "entry-card" >
42- < div className = "entry-card-header" >
43- < button
44- type = "button"
45- onClick = { ( ) => setEditing ( editing === index ? null : index ) }
46- className = "inline-input" style = { { textAlign : 'left' , cursor : 'pointer' } }
47- >
48- < span style = { { color : 'var(--accent-color)' } } > { CLICK_TYPES [ parsed . clickType ] ?? parsed . clickType } </ span >
49- < span > - </ span >
50- { describeAction ( parsed ) }
44+ < div
45+ key = { `${ index } -${ action } ` }
46+ className = "action-item"
47+ draggable
48+ onDragStart = { ( ) => setDragged ( index ) }
49+ onDragEnd = { ( ) => setDragged ( null ) }
50+ onDragOver = { event => event . preventDefault ( ) }
51+ onDrop = { ( ) => {
52+ if ( dragged !== null && dragged !== index ) {
53+ onChange ( move ( actions , dragged , index ) ) ;
54+ }
55+ setDragged ( null ) ;
56+ } }
57+ >
58+ < div className = "action-item-drag" > ⋮⋮</ div >
59+
60+ < div className = "action-item-content" >
61+ < div className = "action-item-header" >
62+ < span className = "action-item-click-type" >
63+ { CLICK_TYPES [ parsed . clickType ] ?? parsed . clickType }
64+ </ span >
65+ < span className = "action-item-type" > { describeAction ( parsed ) } </ span >
66+ </ div >
67+ { parsed . params !== null && < div className = "action-item-params" > { parsed . params } </ div > }
68+ </ div >
69+
70+ < div className = "action-item-actions" >
71+ < button type = "button" className = "btn-icon" title = "Edit" onClick = { ( ) => setEditing ( index ) } >
72+ ✏️
5173 </ button >
5274 < button
5375 type = "button"
76+ className = "btn-icon"
77+ title = "Delete"
5478 onClick = { ( ) => onChange ( actions . filter ( ( _unused , position ) => position !== index ) ) }
55- aria-label = { `Remove action ${ index + 1 } ` }
56- className = "tab-close"
5779 >
58- x
80+ 🗑️
5981 </ button >
6082 </ div >
61-
62- { editing === index && < ActionForm action = { action } onSave = { value => replace ( index , value ) } /> }
63- </ li >
83+ </ div >
6484 ) ;
6585 } ) }
66- </ ul >
67- </ section >
86+ </ div >
87+
88+ { editing !== null && actions [ editing ] !== undefined && (
89+ < ActionDialog
90+ action = { actions [ editing ] }
91+ onSave = { value => {
92+ onChange ( actions . map ( ( existing , position ) => ( position === editing ? value : existing ) ) ) ;
93+ close ( ) ;
94+ } }
95+ onClose = { close }
96+ />
97+ ) }
98+ </ >
6899 ) ;
69100}
70101
71- function ActionForm ( { action, onSave } : { action : string ; onSave : ( action : string ) => void } ) {
102+ function ActionDialog ( {
103+ action,
104+ onSave,
105+ onClose,
106+ } : {
107+ action : string ;
108+ onSave : ( action : string ) => void ;
109+ onClose : ( ) => void ;
110+ } ) {
72111 const parsed = parseAction ( action ) ;
73112 const [ clickType , setClickType ] = useState ( parsed . clickType ) ;
74113 const [ type , setType ] = useState ( parsed . type ) ;
@@ -77,60 +116,98 @@ function ActionForm({ action, onSave }: { action: string; onSave: (action: strin
77116 const definition = ACTION_TYPES [ type ] ;
78117
79118 return (
80- < div className = "editor-field" >
81- < label >
119+ < Modal
120+ title = "Action"
121+ onClose = { onClose }
122+ footer = {
123+ < >
124+ < button type = "button" className = "btn btn-secondary" onClick = { onClose } >
125+ Cancel
126+ </ button >
127+ < button
128+ type = "button"
129+ className = "btn btn-primary"
130+ onClick = { ( ) => onSave ( buildActionString ( clickType , type , params === '' ? null : params ) ) }
131+ >
132+ Save
133+ </ button >
134+ </ >
135+ }
136+ >
137+ < label className = "modal-label" htmlFor = "action-click" >
82138 Click
83- < select
84- value = { clickType }
85- onChange = { event => setClickType ( event . target . value ) }
86- className = "inline-input"
87- >
88- { Object . entries ( CLICK_TYPES ) . map ( ( [ value , label ] ) => (
89- < option key = { value } value = { value } >
90- { label }
91- </ option >
92- ) ) }
93- </ select >
94139 </ label >
140+ < select
141+ id = "action-click"
142+ className = "modal-select"
143+ value = { clickType }
144+ onChange = { event => setClickType ( event . target . value ) }
145+ >
146+ { Object . entries ( CLICK_TYPES ) . map ( ( [ value , label ] ) => (
147+ < option key = { value } value = { value } >
148+ { label }
149+ </ option >
150+ ) ) }
151+ </ select >
95152
96- < label >
153+ < label className = "modal-label" htmlFor = "action-type" >
97154 Action
98- < select
99- value = { type }
100- onChange = { event => {
101- setType ( event . target . value ) ;
102- setParams ( '' ) ;
103- } }
104- className = "inline-input"
105- >
106- { Object . entries ( ACTION_TYPES ) . map ( ( [ value , entry ] ) => (
107- < option key = { value } value = { value } >
108- { entry . name }
109- </ option >
110- ) ) }
111- </ select >
112155 </ label >
156+ < select
157+ id = "action-type"
158+ className = "modal-select"
159+ value = { type }
160+ onChange = { event => {
161+ setType ( event . target . value ) ;
162+ setParams ( '' ) ;
163+ } }
164+ >
165+ { Object . entries ( ACTION_TYPES ) . map ( ( [ value , entry ] ) => (
166+ < option key = { value } value = { value } >
167+ { entry . name }
168+ </ option >
169+ ) ) }
170+ </ select >
171+
172+ { definition !== undefined && < p className = "empty-note" > { definition . description } </ p > }
113173
114174 { definition ?. hasParams && (
115- < label >
116- { definition . fields . map ( field => field . label ) . join ( ' ; ' ) }
117- < input
118- value = { params }
119- onChange = { event => setParams ( event . target . value ) }
120- placeholder = { definition . fields . map ( field => field . placeholder ) . join ( ';' ) }
121- className = "inline-input"
122- />
123- < span className = "field-hint" > { definition . fields [ 0 ] ?. hint } </ span >
124- </ label >
175+ < >
176+ < label className = "modal-label" htmlFor = "action-params" >
177+ { definition . fields . map ( field => field . label ) . join ( ' ; ' ) }
178+ </ label >
179+ { definition . fields [ 0 ] ?. type === 'textarea' ? (
180+ < textarea
181+ id = "action-params"
182+ className = "modal-input"
183+ rows = { 3 }
184+ value = { params }
185+ placeholder = { definition . fields [ 0 ] ?. placeholder }
186+ onChange = { event => setParams ( event . target . value ) }
187+ />
188+ ) : (
189+ < input
190+ id = "action-params"
191+ className = "modal-input"
192+ value = { params }
193+ placeholder = { definition . fields . map ( field => field . placeholder ) . join ( ';' ) }
194+ onChange = { event => setParams ( event . target . value ) }
195+ />
196+ ) }
197+ < p className = "empty-note" > { definition . fields [ 0 ] ?. hint } </ p >
198+ </ >
125199 ) }
126-
127- < button
128- type = "button"
129- onClick = { ( ) => onSave ( buildActionString ( clickType , type , params === '' ? null : params ) ) }
130- className = "btn btn-primary btn-sm"
131- >
132- Apply
133- </ button >
134- </ div >
200+ </ Modal >
135201 ) ;
136202}
203+
204+ function move ( actions : string [ ] , from : number , to : number ) : string [ ] {
205+ const next = [ ...actions ] ;
206+ const [ moved ] = next . splice ( from , 1 ) ;
207+
208+ if ( moved !== undefined ) {
209+ next . splice ( to , 0 , moved ) ;
210+ }
211+
212+ return next ;
213+ }
0 commit comments