@@ -9,6 +9,18 @@ import {
9
9
NonIdealState ,
10
10
TextArea ,
11
11
} from '@blueprintjs/core'
12
+ import {
13
+ DndContext ,
14
+ DragEndEvent ,
15
+ PointerSensor ,
16
+ useSensor ,
17
+ useSensors ,
18
+ } from '@dnd-kit/core'
19
+ import {
20
+ SortableContext ,
21
+ arrayMove ,
22
+ verticalListSortingStrategy ,
23
+ } from '@dnd-kit/sortable'
12
24
13
25
import { useOperations } from 'apis/operation'
14
26
import {
@@ -20,11 +32,21 @@ import {
20
32
useRefreshOperationSets ,
21
33
} from 'apis/operation-set'
22
34
import clsx from 'clsx'
23
- import { Ref , useCallback , useImperativeHandle , useRef , useState } from 'react'
35
+ import { UpdateCopilotSetRequest } from 'maa-copilot-client'
36
+ import {
37
+ Ref ,
38
+ useCallback ,
39
+ useEffect ,
40
+ useImperativeHandle ,
41
+ useRef ,
42
+ useState ,
43
+ } from 'react'
24
44
import { Controller , UseFormSetError , useForm } from 'react-hook-form'
25
45
26
46
import { FormField } from 'components/FormField'
27
47
import { AppToaster } from 'components/Toaster'
48
+ import { Sortable } from 'components/dnd'
49
+ import { Operation } from 'models/operation'
28
50
import { OperationSet } from 'models/operation-set'
29
51
import { formatError } from 'utils/error'
30
52
@@ -67,15 +89,20 @@ export function OperationSetEditorDialog({
67
89
status,
68
90
idsToAdd,
69
91
idsToRemove,
92
+ sortIds,
70
93
} ) => {
71
94
const updateInfo = async ( ) => {
72
95
if ( isEdit ) {
73
- await updateOperationSet ( {
96
+ const params : UpdateCopilotSetRequest [ 'copilotSetUpdateReq' ] = {
74
97
id : operationSet ! . id ,
75
98
name,
76
99
description,
77
100
status,
78
- } )
101
+ }
102
+
103
+ if ( sortIds ?. length ) params . copilotIds = sortIds
104
+
105
+ await updateOperationSet ( params )
79
106
80
107
AppToaster . show ( {
81
108
intent : 'success' ,
@@ -168,6 +195,7 @@ interface FormValues {
168
195
169
196
idsToAdd ?: number [ ]
170
197
idsToRemove ?: number [ ]
198
+ sortIds ?: number [ ]
171
199
}
172
200
173
201
function OperationSetForm ( { operationSet, onSubmit } : FormProps ) {
@@ -207,7 +235,7 @@ function OperationSetForm({ operationSet, onSubmit }: FormProps) {
207
235
return (
208
236
< form
209
237
className = { clsx (
210
- 'p-4 w-[500px] max-w-[100vw] max-h-[calc(100vh-20rem)] overflow-y-auto' ,
238
+ 'p-4 w-[500px] max-w-[100vw] max-h-[calc(100vh-20rem)] min-h-[18rem] overflow-y-auto' ,
211
239
isEdit && 'lg:w-[1000px]' ,
212
240
) }
213
241
onSubmit = { localOnSubmit }
@@ -219,6 +247,7 @@ function OperationSetForm({ operationSet, onSubmit }: FormProps) {
219
247
< >
220
248
< div className = "grow" >
221
249
< OperationSelector
250
+ key = { operationSet . id }
222
251
operationSet = { operationSet }
223
252
selectorRef = { operationSelectorRef }
224
253
/>
@@ -330,7 +359,11 @@ interface OperationSelectorProps {
330
359
}
331
360
332
361
interface OperationSelectorRef {
333
- getValues ( ) : { idsToAdd : number [ ] ; idsToRemove : number [ ] }
362
+ getValues ( ) : {
363
+ idsToAdd : number [ ]
364
+ idsToRemove : number [ ]
365
+ sortIds ?: number [ ]
366
+ }
334
367
}
335
368
336
369
function OperationSelector ( {
@@ -341,6 +374,14 @@ function OperationSelector({
341
374
operationIds : operationSet . copilotIds ,
342
375
} )
343
376
377
+ const [ isSorting , setIsSorting ] = useState ( false )
378
+
379
+ const [ renderedOperations , setRenderedOperations ] = useState < Operation [ ] > ( [ ] )
380
+ useEffect ( ( ) => {
381
+ setRenderedOperations ( [ ...( operations ?? [ ] ) ] )
382
+ // eslint-disable-next-line react-hooks/exhaustive-deps
383
+ } , [ operations . length ] )
384
+
344
385
const [ checkboxOverrides , setCheckboxOverrides ] = useState (
345
386
{ } as Record < number , boolean > ,
346
387
)
@@ -356,7 +397,7 @@ function OperationSelector({
356
397
getValues ( ) {
357
398
const idsToAdd : number [ ] = [ ]
358
399
const idsToRemove : number [ ] = [ ]
359
-
400
+ const sortIds : number [ ] = [ ]
360
401
Object . entries ( checkboxOverrides ) . forEach ( ( [ idKey , checked ] ) => {
361
402
const id = + idKey
362
403
if ( isNaN ( id ) ) return
@@ -367,13 +408,37 @@ function OperationSelector({
367
408
idsToRemove . push ( id )
368
409
}
369
410
} )
370
-
371
- return { idsToAdd, idsToRemove }
411
+ if ( isSorting ) {
412
+ sortIds . push (
413
+ ...renderedOperations
414
+ . map ( ( { id } ) => ( checkboxOverrides [ id ] === false ? 0 : id ) )
415
+ . filter ( ( id ) => ! ! id ) ,
416
+ )
417
+ }
418
+
419
+ return { idsToAdd, idsToRemove, sortIds }
372
420
} ,
373
421
} ) ,
374
- [ checkboxOverrides , alreadyAdded ] ,
422
+ [ checkboxOverrides , isSorting , alreadyAdded , renderedOperations ] ,
375
423
)
376
424
425
+ const sensors = useSensors ( useSensor ( PointerSensor ) )
426
+
427
+ const handleDragEnd = ( event : DragEndEvent ) => {
428
+ const { active, over } = event
429
+
430
+ if ( active . id !== over ?. id ) {
431
+ setRenderedOperations ( ( items ) => {
432
+ const oldIndex = items . findIndex ( ( v ) => v . id === active . id )
433
+ const newIndex = items . findIndex ( ( v ) => v . id === over ?. id )
434
+
435
+ return arrayMove ( items , oldIndex , newIndex )
436
+ } )
437
+
438
+ setIsSorting ( true )
439
+ }
440
+ }
441
+
377
442
return (
378
443
< div className = "py-2" >
379
444
{ error && (
@@ -382,28 +447,53 @@ function OperationSelector({
382
447
</ Callout >
383
448
) }
384
449
385
- { operations ?. map ( ( { id, parsedContent } ) => (
386
- < div key = { id } >
387
- < Checkbox
388
- className = { clsx (
389
- 'flex items-center m-0 p-2 !pl-10 hover:bg-slate-200' ,
390
- checkboxOverrides [ id ] !== undefined &&
391
- checkboxOverrides [ id ] !== alreadyAdded ( id ) &&
392
- 'font-bold' ,
393
- ) }
394
- checked = { checkboxOverrides [ id ] ?? alreadyAdded ( id ) }
395
- onChange = { ( e ) => {
396
- const checked = ( e . target as HTMLInputElement ) . checked
397
- setCheckboxOverrides ( ( prev ) => ( { ...prev , [ id ] : checked } ) )
398
- } }
399
- >
400
- < div className = "tabular-nums text-slate-500" > { id } : </ div >
401
- < div className = "truncate text-ellipsis" >
402
- { parsedContent . doc . title }
403
- </ div >
404
- </ Checkbox >
405
- </ div >
406
- ) ) }
450
+ < DndContext sensors = { sensors } onDragEnd = { handleDragEnd } >
451
+ < SortableContext
452
+ items = { ( renderedOperations ?? [ ] ) . map ( ( { id } ) => id ) }
453
+ strategy = { verticalListSortingStrategy }
454
+ >
455
+ { renderedOperations ?. map ( ( { id, parsedContent } ) => (
456
+ < Sortable key = { id } id = { id } >
457
+ { ( { listeners, attributes } ) => (
458
+ < div
459
+ key = { id }
460
+ className = "flex items-center hover:bg-slate-200 dark:hover:bg-slate-800"
461
+ >
462
+ < Icon
463
+ className = "cursor-grab active:cursor-grabbing p-1 -my-1 -ml-2 -mr-1 rounded-[1px]"
464
+ icon = "drag-handle-vertical"
465
+ { ...listeners }
466
+ { ...attributes }
467
+ />
468
+ < Checkbox
469
+ className = { clsx (
470
+ 'flex items-center m-0 p-2 !pl-10 flex-1' ,
471
+ checkboxOverrides [ id ] !== undefined &&
472
+ checkboxOverrides [ id ] !== alreadyAdded ( id ) &&
473
+ 'font-bold' ,
474
+ ) }
475
+ checked = { checkboxOverrides [ id ] ?? alreadyAdded ( id ) }
476
+ onChange = { ( e ) => {
477
+ const checked = ( e . target as HTMLInputElement ) . checked
478
+ setCheckboxOverrides ( ( prev ) => ( {
479
+ ...prev ,
480
+ [ id ] : checked ,
481
+ } ) )
482
+ } }
483
+ >
484
+ < div className = "tabular-nums text-slate-500" >
485
+ { id } :
486
+ </ div >
487
+ < div className = "truncate text-ellipsis" >
488
+ { parsedContent . doc . title }
489
+ </ div >
490
+ </ Checkbox >
491
+ </ div >
492
+ ) }
493
+ </ Sortable >
494
+ ) ) }
495
+ </ SortableContext >
496
+ </ DndContext >
407
497
</ div >
408
498
)
409
499
}
0 commit comments