@@ -13,8 +13,8 @@ import {
1313 useUpdatableTree ,
1414 ViewMode ,
1515} from "./edit-state" ;
16- import { useCallback , useEffect , useRef } from "react" ;
17- import { ButtonGroup , Card , SegmentedControl } from "@blueprintjs/core" ;
16+ import { useCallback , useEffect , useRef , useState } from "react" ;
17+ import { ButtonGroup , Card , SegmentedControl , Tag } from "@blueprintjs/core" ;
1818import { OmniboxSelector } from "./type-selector" ;
1919import {
2020 CancelButton ,
@@ -25,6 +25,8 @@ import {
2525} from "@macrostrat/ui-components" ;
2626import useElementDimensions from "use-element-dimensions" ;
2727import { GraphView } from "./graph" ;
28+ import { useInDarkMode } from "@macrostrat/ui-components" ;
29+ import { asChromaColor } from "@macrostrat/color-utils" ;
2830
2931export type { GraphData } from "./edit-state" ;
3032export { treeToGraph } from "./edit-state" ;
@@ -49,7 +51,6 @@ export function FeedbackComponent({
4951 onSave,
5052} ) {
5153 // Get the input arguments
52-
5354 const [ state , dispatch ] = useUpdatableTree (
5455 entities . map ( processEntity ) as any ,
5556 entityTypes
@@ -128,6 +129,9 @@ export function FeedbackComponent({
128129 onChange ( payload ) {
129130 dispatch ( { type : "select-entity-type" , payload } ) ;
130131 } ,
132+ dispatch,
133+ tree,
134+ selectedNodes,
131135 isOpen : isSelectingEntityType ,
132136 setOpen : ( isOpen : boolean ) =>
133137 dispatch ( {
@@ -171,34 +175,68 @@ function EntityTypeSelector({
171175 isOpen,
172176 setOpen,
173177 onChange,
178+ tree,
179+ dispatch,
180+ selectedNodes = [ ] ,
174181} ) {
175182 // Show all entity types when selected is null
176183 const _selected = selected != null ? selected : undefined ;
177- return h ( DataField , { label : "Entity type" , inline : true } , [
184+ const [ inputValue , setInputValue ] = useState ( "" ) ;
185+ const types = Array . from ( entityTypes . values ( ) ) ;
186+
187+ const items = inputValue !== "" ? types . filter ( ( d ) =>
188+ d . name . toLowerCase ( ) . includes ( inputValue . toLowerCase ( ) )
189+ ) : types ;
190+
191+ return h ( 'div.entity-type-selector' , [
192+ /*
178193 h(
179194 "code.bp5-code",
180195 {
181196 onClick() {
182197 setOpen((d) => !d);
183198 },
184199 },
185- selected . name
200+ selected? .name ?? "None"
186201 ),
202+ */
203+ h ( 'p' , "Entity Type:" ) ,
204+ h ( TypeList , { types : entityTypes , selected : _selected , dispatch, tree, selectedNodes } ) ,
187205 h ( OmniboxSelector , {
188206 isOpen,
189- items : Array . from ( entityTypes . values ( ) ) ,
207+ items,
190208 selectedItem : _selected ,
191209 onSelectItem ( item ) {
192210 setOpen ( false ) ;
193211 onChange ( item ) ;
194212 } ,
213+ onQueryChange ( query ) {
214+ setInputValue ( query ) ;
215+ } ,
195216 onClose ( ) {
196217 setOpen ( false ) ;
197218 } ,
198219 } ) ,
199220 ] ) ;
200221}
201222
223+ function countNodes ( tree ) {
224+ if ( ! tree ) return 0 ;
225+ let count = 0 ;
226+
227+ function recurse ( nodes ) {
228+ for ( const node of nodes ) {
229+ count ++ ;
230+ if ( node . children && Array . isArray ( node . children ) ) {
231+ recurse ( node . children ) ;
232+ }
233+ }
234+ }
235+
236+ recurse ( tree ) ;
237+ return count ;
238+ }
239+
202240function ManagedSelectionTree ( props ) {
203241 const {
204242 selectedNodes,
@@ -207,65 +245,128 @@ function ManagedSelectionTree(props) {
207245 height,
208246 width,
209247 matchComponent,
210- ...rest
211248 } = props ;
212249
213250 const ref = useRef < TreeApi < TreeData > > ( ) ;
251+ // Use a ref to track clicks (won't cause rerender)
252+ const clickedRef = useRef ( false ) ;
214253
215254 const _Node = useCallback (
216255 ( props ) => h ( Node , { ...props , matchComponent } ) ,
217256 [ matchComponent ]
218257 ) ;
219258
259+ // Update Tree selection when selectedNodes change
220260 useEffect ( ( ) => {
221261 if ( ref . current == null ) return ;
222- // Check if selection matches current
262+
223263 const selection = new Set ( selectedNodes . map ( ( d ) => d . toString ( ) ) ) ;
224264 const currentSelection = ref . current . selectedIds ;
225265 if ( setsAreTheSame ( selection , currentSelection ) ) return ;
226- // If the selection is the same, do nothing
227266
228- // Set selection
229267 ref . current . setSelection ( {
230268 ids : selectedNodes . map ( ( d ) => d . toString ( ) ) ,
231269 anchor : null ,
232270 mostRecent : null ,
233271 } ) ;
234272 } , [ selectedNodes ] ) ;
235273
236- return h ( Tree , {
237- className : "selection-tree" ,
238- height,
239- width,
240- ref,
241- data : tree ,
242- onMove ( { dragIds, parentId, index } ) {
243- dispatch ( {
244- type : "move-node" ,
245- payload : {
246- dragIds : dragIds . map ( ( d ) => parseInt ( d ) ) ,
247- parentId : parentId ? parseInt ( parentId ) : null ,
248- index,
249- } ,
250- } ) ;
251- } ,
252- onDelete ( { ids } ) {
253- dispatch ( {
254- type : "delete-node" ,
255- payload : { ids : ids . map ( ( d ) => parseInt ( d ) ) } ,
256- } ) ;
257- } ,
258- onSelect ( nodes ) {
274+ // Mark clicked when user clicks inside the tree container
275+ function handleClick ( ) {
276+ clickedRef . current = true ;
277+ }
278+
279+ const handleSelect = useCallback (
280+ ( nodes ) => {
281+ if ( ! clickedRef . current ) return ;
282+ clickedRef . current = false ;
283+
259284 let ids = nodes . map ( ( d ) => parseInt ( d . id ) ) ;
260- if ( ids . length == 1 && ids [ 0 ] == selectedNodes [ 0 ] ) {
261- // Deselect
285+ if ( ids . length === 1 && ids [ 0 ] === selectedNodes [ 0 ] ) {
262286 ids = [ ] ;
263287 }
288+
264289 dispatch ( { type : "select-node" , payload : { ids } } ) ;
265290 } ,
266- children : _Node ,
267- idAccessor ( d : TreeData ) {
268- return d . id . toString ( ) ;
269- } ,
270- } ) ;
291+ [ selectedNodes , dispatch ]
292+ ) ;
293+
294+ return h (
295+ "div.selection-tree-wrapper" ,
296+ { onPointerDown : handleClick } ,
297+ h ( Tree , {
298+ className : "selection-tree" ,
299+ height,
300+ width,
301+ ref,
302+ data : tree ,
303+ onMove ( { dragIds, parentId, index } ) {
304+ dispatch ( {
305+ type : "move-node" ,
306+ payload : {
307+ dragIds : dragIds . map ( ( d ) => parseInt ( d ) ) ,
308+ parentId : parentId ? parseInt ( parentId ) : null ,
309+ index,
310+ } ,
311+ } ) ;
312+ } ,
313+ onDelete ( { ids } ) {
314+ dispatch ( {
315+ type : "delete-node" ,
316+ payload : { ids : ids . map ( ( d ) => parseInt ( d ) ) } ,
317+ } ) ;
318+ } ,
319+ onSelect : handleSelect ,
320+ children : _Node ,
321+ idAccessor ( d ) {
322+ return d . id . toString ( ) ;
323+ } ,
324+ } )
325+ ) ;
326+ }
327+
328+ function TypeList ( { types, selected, dispatch, tree, selectedNodes } ) {
329+ return h (
330+ "div.type-list" ,
331+ Array . from ( types . values ( ) ) . map ( ( type ) => {
332+ const { color, name, id } = type ;
333+ const darkMode = useInDarkMode ( ) ;
334+ const luminance = darkMode ? 0.9 : 0.4 ;
335+ const chromaColor = asChromaColor ( color ?? "#000000" )
336+ const ids = collectMatchingIds ( tree , name ) ;
337+ const alreadySelected = ids . length === selectedNodes . length && ids . every ( ( v , i ) => v === selectedNodes [ i ] )
338+
339+ return h (
340+ Tag ,
341+ {
342+ className : "type-tag" ,
343+ style : {
344+ color : chromaColor ?. luminance ( luminance ) . hex ( ) ,
345+ backgroundColor : chromaColor ?. luminance ( 1 - luminance ) . hex ( ) ,
346+ border : id === selected ?. id ? `1px solid white` : `1px solid black` ,
347+ } ,
348+ onClick : ( ) => {
349+ dispatch ( { type : "select-node" , payload : { ids : alreadySelected ? [ ] : ids } } ) ;
350+ }
351+ } ,
352+ name
353+ ) ;
354+ } )
355+ ) ;
356+ }
357+
358+ function collectMatchingIds ( tree , name ) {
359+ const ids = [ ] ;
360+
361+ function traverse ( node ) {
362+ if ( node . term_type === name ) {
363+ ids . push ( node . id ) ;
364+ }
365+ if ( Array . isArray ( node . children ) ) {
366+ node . children . forEach ( traverse ) ;
367+ }
368+ }
369+
370+ tree . forEach ( traverse ) ;
371+ return ids ;
271372}
0 commit comments