@@ -9,10 +9,11 @@ import {
99 ModalHeader ,
1010 TextInput ,
1111} from '@carbon/react' ;
12+ import { ArrowUp , ArrowDown , Close } from '@carbon/icons-react' ;
1213import { byIdOptions } from '@client/@tanstack/react-query.gen.ts' ;
1314import { ViewService } from '@client/sdk.gen.ts' ;
1415import { useMutation , useQueryClient , useSuspenseQuery } from '@tanstack/react-query' ;
15- import { useCallback , useEffect , useState } from 'react' ;
16+ import { useCallback , useState } from 'react' ;
1617
1718interface ViewConfigModalProps {
1819 open : boolean ;
@@ -34,26 +35,8 @@ export const ViewConfigModal = ({ open, onClose, folderName, groupId, view }: Vi
3435 const isEditing = view != null && view . id != null ;
3536 const isDefault = view ?. name === 'Default' ;
3637
37- const [ viewName , setViewName ] = useState ( view ?. name ?? '' ) ;
38- const [ selectedNodes , setSelectedNodes ] = useState < NodeItem [ ] > ( [ ] ) ;
39-
40- // Initialize from existing view when editing
41- useEffect ( ( ) => {
42- if ( view ) {
43- setViewName ( view . name ) ;
44- const items = ( view . components ?? [ ] ) . map ( ( c : ViewComponent ) => ( {
45- id : String ( c . nodeId ) ,
46- text : c . headerName ?? c . nodeName ?? '' ,
47- nodeId : c . nodeId ! ,
48- } ) ) ;
49- setSelectedNodes ( items ) ;
50- } else {
51- setViewName ( '' ) ;
52- setSelectedNodes ( [ ] ) ;
53- }
54- } , [ view ] ) ;
55-
5638 // Available nodes for the multi-select (exclude detection nodes)
39+ // Built once and stable so FilterableMultiSelect can match by reference
5740 const availableNodes : NodeItem [ ] = ( nodeGroup . sources ?? [ ] )
5841 . filter ( ( n : ApiNode ) => ! [ 'FIXED_THRESHOLD' , 'RELATIVE_DIFFERENCE' , 'EDIVISIVE' ] . includes ( n . type ?? '' ) )
5942 . map ( ( n : ApiNode ) => ( {
@@ -62,16 +45,70 @@ export const ViewConfigModal = ({ open, onClose, folderName, groupId, view }: Vi
6245 nodeId : n . id ! ,
6346 } ) ) ;
6447
48+ const [ viewName , setViewName ] = useState ( view ?. name ?? '' ) ;
49+ // Initialize selectedNodes from the view's components, preserving the
50+ // existing headerOrder. Find matching items in availableNodes (same
51+ // object references required by Carbon's FilterableMultiSelect).
52+ const [ selectedNodes , setSelectedNodes ] = useState < NodeItem [ ] > ( ( ) => {
53+ if ( ! view ?. components || view . components . length === 0 ) return [ ] ;
54+ // Sort components by headerOrder to preserve column ordering
55+ const sorted = [ ...view . components ] . sort (
56+ ( a : ViewComponent , b : ViewComponent ) => ( a . headerOrder ?? 0 ) - ( b . headerOrder ?? 0 )
57+ ) ;
58+ const ordered : NodeItem [ ] = [ ] ;
59+ for ( const c of sorted ) {
60+ const match = availableNodes . find ( ( n ) => n . id === String ( c . nodeId ) ) ;
61+ if ( match ) ordered . push ( match ) ;
62+ }
63+ return ordered ;
64+ } ) ;
65+
66+ const moveUp = useCallback ( ( index : number ) => {
67+ if ( index <= 0 ) return ;
68+ setSelectedNodes ( ( prev ) => {
69+ const next = [ ...prev ] ;
70+ const temp = next [ index - 1 ] ! ;
71+ next [ index - 1 ] = next [ index ] ! ;
72+ next [ index ] = temp ;
73+ return next ;
74+ } ) ;
75+ } , [ ] ) ;
76+
77+ const moveDown = useCallback ( ( index : number ) => {
78+ setSelectedNodes ( ( prev ) => {
79+ if ( index >= prev . length - 1 ) return prev ;
80+ const next = [ ...prev ] ;
81+ const temp = next [ index ] ! ;
82+ next [ index ] = next [ index + 1 ] ! ;
83+ next [ index + 1 ] = temp ;
84+ return next ;
85+ } ) ;
86+ } , [ ] ) ;
87+
88+ const removeNode = useCallback ( ( index : number ) => {
89+ setSelectedNodes ( ( prev ) => prev . filter ( ( _ , i ) => i !== index ) ) ;
90+ } , [ ] ) ;
91+
92+ const [ saveError , setSaveError ] = useState < string | null > ( null ) ;
93+
6594 const createMutation = useMutation ( {
6695 mutationFn : ( data : View ) =>
6796 ViewService . createView ( {
6897 path : { name : folderName } ,
6998 body : data ,
7099 } ) ,
71100 onSuccess : ( ) => {
72- queryClient . invalidateQueries ( { queryKey : [ 'getViews' ] } ) ;
101+ setSaveError ( null ) ;
102+ void queryClient . refetchQueries ( { predicate : ( q ) => {
103+ const key = q . queryKey [ 0 ] ;
104+ return typeof key === 'object' && key !== null && '_id' in key &&
105+ ( key . _id === 'getViews' || key . _id === 'getViewData' ) ;
106+ } } ) ;
73107 onClose ( ) ;
74108 } ,
109+ onError : ( e : Error ) => {
110+ setSaveError ( e . message ?? 'Failed to create view' ) ;
111+ } ,
75112 } ) ;
76113
77114 const updateMutation = useMutation ( {
@@ -81,9 +118,17 @@ export const ViewConfigModal = ({ open, onClose, folderName, groupId, view }: Vi
81118 body : data ,
82119 } ) ,
83120 onSuccess : ( ) => {
84- queryClient . invalidateQueries ( { queryKey : [ 'getViews' ] } ) ;
121+ setSaveError ( null ) ;
122+ void queryClient . refetchQueries ( { predicate : ( q ) => {
123+ const key = q . queryKey [ 0 ] ;
124+ return typeof key === 'object' && key !== null && '_id' in key &&
125+ ( key . _id === 'getViews' || key . _id === 'getViewData' ) ;
126+ } } ) ;
85127 onClose ( ) ;
86128 } ,
129+ onError : ( e : Error ) => {
130+ setSaveError ( e . message ?? 'Failed to update view' ) ;
131+ } ,
87132 } ) ;
88133
89134 const deleteMutation = useMutation ( {
@@ -92,7 +137,11 @@ export const ViewConfigModal = ({ open, onClose, folderName, groupId, view }: Vi
92137 path : { name : folderName , viewId : view ! . id ! } ,
93138 } ) ,
94139 onSuccess : ( ) => {
95- queryClient . invalidateQueries ( { queryKey : [ 'getViews' ] } ) ;
140+ void queryClient . refetchQueries ( { predicate : ( q ) => {
141+ const key = q . queryKey [ 0 ] ;
142+ return typeof key === 'object' && key !== null && '_id' in key &&
143+ ( key . _id === 'getViews' || key . _id === 'getViewData' ) ;
144+ } } ) ;
96145 onClose ( ) ;
97146 } ,
98147 } ) ;
@@ -144,9 +193,71 @@ export const ViewConfigModal = ({ open, onClose, folderName, groupId, view }: Vi
144193 itemToString = { ( item : NodeItem ) => item ?. text ?? '' }
145194 initialSelectedItems = { selectedNodes }
146195 onChange = { ( { selectedItems } : { selectedItems : NodeItem [ ] } ) => {
147- setSelectedNodes ( selectedItems ) ;
196+ // Preserve existing order: keep items that are still selected
197+ // in their current position, append newly added items at the end
198+ const existingIds = new Set ( selectedNodes . map ( ( n ) => n . id ) ) ;
199+ const newIds = new Set ( selectedItems . map ( ( n ) => n . id ) ) ;
200+ const kept = selectedNodes . filter ( ( n ) => newIds . has ( n . id ) ) ;
201+ const added = selectedItems . filter ( ( n ) => ! existingIds . has ( n . id ) ) ;
202+ setSelectedNodes ( [ ...kept , ...added ] ) ;
148203 } }
149204 />
205+ { selectedNodes . length > 0 && (
206+ < div style = { { marginTop : 'var(--cds-spacing-05)' } } >
207+ < div style = { { fontSize : '0.75rem' , opacity : 0.7 , marginBottom : 'var(--cds-spacing-02)' } } >
208+ Column order (drag or use arrows to reorder)
209+ </ div >
210+ { selectedNodes . map ( ( node , idx ) => (
211+ < div
212+ key = { node . id }
213+ style = { {
214+ display : 'flex' ,
215+ alignItems : 'center' ,
216+ gap : 'var(--cds-spacing-02)' ,
217+ padding : '4px 8px' ,
218+ marginBottom : '2px' ,
219+ background : 'var(--cds-layer-02)' ,
220+ borderRadius : '4px' ,
221+ fontSize : '0.875rem' ,
222+ } }
223+ >
224+ < span style = { { opacity : 0.5 , minWidth : '20px' } } > { String ( idx + 1 ) } .</ span >
225+ < span style = { { flex : 1 } } > { node . text } </ span >
226+ < Button
227+ kind = "ghost"
228+ size = "sm"
229+ hasIconOnly
230+ renderIcon = { ArrowUp }
231+ iconDescription = "Move up"
232+ onClick = { ( ) => moveUp ( idx ) }
233+ disabled = { idx === 0 }
234+ />
235+ < Button
236+ kind = "ghost"
237+ size = "sm"
238+ hasIconOnly
239+ renderIcon = { ArrowDown }
240+ iconDescription = "Move down"
241+ onClick = { ( ) => moveDown ( idx ) }
242+ disabled = { idx === selectedNodes . length - 1 }
243+ />
244+ < Button
245+ kind = "ghost"
246+ size = "sm"
247+ hasIconOnly
248+ renderIcon = { Close }
249+ iconDescription = "Remove"
250+ onClick = { ( ) => removeNode ( idx ) }
251+ />
252+ </ div >
253+ ) ) }
254+ </ div >
255+ ) }
256+ { saveError && (
257+ < div style = { { color : 'var(--cds-support-error)' , marginTop : 'var(--cds-spacing-03)' } } >
258+ { saveError }
259+ </ div >
260+ ) }
150261 </ ModalBody >
151262 < ModalFooter >
152263 { isEditing && ! isDefault && (
0 commit comments