55 useMemo ,
66 useState ,
77} from 'react' ;
8+ import { ContentHit } from 'search-manager' ;
9+ import { useSearchContext } from 'search-manager/SearchManager' ;
810
911export interface SelectedComponent {
1012 usageKey : string ;
@@ -19,9 +21,14 @@ export interface SelectedCollection {
1921 status : CollectionStatus ;
2022}
2123
24+ export interface CollectionData {
25+ components : SelectedComponent [ ] ;
26+ affectedCollectionSizes : Map < string , number > ;
27+ }
28+
2229export type ComponentSelectedEvent = (
2330 selectedComponent : SelectedComponent ,
24- collectionComponents ?: SelectedComponent [ ] | number
31+ collectionComponents ?: CollectionData | Map < string , number >
2532) => void ;
2633export type ComponentSelectionChangedEvent = ( selectedComponents : SelectedComponent [ ] ) => void ;
2734
@@ -88,6 +95,87 @@ type ComponentPickerProviderProps = {
8895 children ?: React . ReactNode ;
8996} & ComponentPickerProps ;
9097
98+ /**
99+ * Pre-computed collection indexing data for O(1) lookups
100+ */
101+ export interface CollectionIndexData {
102+ /** Map: collectionKey → components in that collection */
103+ collectionToComponents : Map < string , SelectedComponent [ ] > ;
104+ /** Map: componentUsageKey → collection keys it belongs to */
105+ componentToCollections : Map < string , string [ ] > ;
106+ /** Map: collectionKey → Map of all affected collections with their sizes */
107+ collectionToAffectedSizes : Map < string , Map < string , number > > ;
108+ /** Map: collectionKey → total component count (for quick size lookup) */
109+ collectionSizes : Map < string , number > ;
110+ }
111+
112+ /**
113+ * Hook to build indexing maps for collections and components.
114+ * Pre-computes all relationships for O(1) lookups during selection operations.
115+ * @param hits - Search hits from which to build the indexes
116+ * @returns Pre-computed collection index data
117+ */
118+ export const useCollectionIndexing = (
119+ hits : ReturnType < typeof useSearchContext > [ 'hits' ] ,
120+ ) : CollectionIndexData => useMemo ( ( ) => {
121+ const collectionToComponents = new Map < string , SelectedComponent [ ] > ( ) ;
122+ const componentToCollections = new Map < string , string [ ] > ( ) ;
123+ const collectionSizes = new Map < string , number > ( ) ;
124+
125+ // First pass: build basic indexes
126+ hits . forEach ( ( hit ) => {
127+ if ( hit . type === 'library_block' ) {
128+ const collectionKeys = ( hit as ContentHit ) . collections ?. key ?? [ ] ;
129+
130+ // Index component → collections mapping
131+ if ( hit . usageKey ) {
132+ componentToCollections . set ( hit . usageKey , collectionKeys ) ;
133+ }
134+
135+ // Index collection → components mapping
136+ collectionKeys . forEach ( ( collectionKey : string ) => {
137+ if ( ! collectionToComponents . has ( collectionKey ) ) {
138+ collectionToComponents . set ( collectionKey , [ ] ) ;
139+ }
140+ collectionToComponents . get ( collectionKey ) ! . push ( {
141+ usageKey : hit . usageKey ,
142+ blockType : hit . blockType ,
143+ collectionKeys,
144+ } ) ;
145+ } ) ;
146+ }
147+ } ) ;
148+
149+ // Second pass: compute collection sizes
150+ collectionToComponents . forEach ( ( components , collectionKey ) => {
151+ collectionSizes . set ( collectionKey , components . length ) ;
152+ } ) ;
153+
154+ // Third pass: pre-compute affected collections for each collection
155+ const collectionToAffectedSizes = new Map < string , Map < string , number > > ( ) ;
156+ collectionToComponents . forEach ( ( components , collectionKey ) => {
157+ const affectedSizes = new Map < string , number > ( ) ;
158+
159+ // For each component in this collection, find all collections it belongs to
160+ components . forEach ( ( component ) => {
161+ component . collectionKeys ?. forEach ( ( affectedKey ) => {
162+ if ( ! affectedSizes . has ( affectedKey ) ) {
163+ affectedSizes . set ( affectedKey , collectionSizes . get ( affectedKey ) ?? 0 ) ;
164+ }
165+ } ) ;
166+ } ) ;
167+
168+ collectionToAffectedSizes . set ( collectionKey , affectedSizes ) ;
169+ } ) ;
170+
171+ return {
172+ collectionToComponents,
173+ componentToCollections,
174+ collectionToAffectedSizes,
175+ collectionSizes,
176+ } ;
177+ } , [ hits ] ) ;
178+
91179/**
92180 * React component to provide `ComponentPickerContext`
93181 */
@@ -101,59 +189,13 @@ export const ComponentPickerProvider = ({
101189 const [ selectedComponents , setSelectedComponents ] = useState < SelectedComponent [ ] > ( [ ] ) ;
102190 const [ selectedCollections , setSelectedCollections ] = useState < SelectedCollection [ ] > ( [ ] ) ;
103191
104- /**
105- * Updates the selectedCollections state based on how many components are selected.
106- * @param collectionKey - The key of the collection to update
107- * @param selectedCount - Number of components currently selected in the collection
108- * @param totalCount - Total number of components in the collection
109- */
110- const updateCollectionStatus = useCallback ( (
111- collectionKey : string ,
112- selectedCount : number ,
113- totalCount : number ,
114- ) => {
115- setSelectedCollections ( ( prevSelectedCollections ) => {
116- const filteredCollections = prevSelectedCollections . filter (
117- ( collection ) => collection . key !== collectionKey ,
118- ) ;
119-
120- if ( selectedCount === 0 ) {
121- return filteredCollections ;
122- }
123- if ( selectedCount >= totalCount ) {
124- return [ ...filteredCollections , { key : collectionKey , status : 'selected' as CollectionStatus } ] ;
125- }
126- return [ ...filteredCollections , { key : collectionKey , status : 'indeterminate' as CollectionStatus } ] ;
127- } ) ;
128- } , [ ] ) ;
129-
130- /**
131- * Finds the common collection key between a component and selected components.
132- */
133- const findCommonCollectionKey = useCallback ( (
134- componentKeys : string [ ] | undefined ,
135- components : SelectedComponent [ ] ,
136- ) : string | undefined => {
137- if ( ! componentKeys ?. length || ! components . length ) {
138- return undefined ;
139- }
140-
141- for ( const component of components ) {
142- const commonKey = component . collectionKeys ?. find ( ( key ) => componentKeys . includes ( key ) ) ;
143- if ( commonKey ) {
144- return commonKey ;
145- }
146- }
147-
148- return undefined ;
149- } , [ ] ) ;
150-
151192 const addComponentToSelectedComponents = useCallback < ComponentSelectedEvent > ( (
152193 selectedComponent : SelectedComponent ,
153- collectionComponents ?: SelectedComponent [ ] | number ,
194+ collectionComponents ?: CollectionData | Map < string , number > ,
154195 ) => {
155- const componentsToAdd = Array . isArray ( collectionComponents ) && collectionComponents . length
156- ? collectionComponents
196+ const isCollectionSelection = collectionComponents && 'components' in collectionComponents ;
197+ const componentsToAdd = isCollectionSelection
198+ ? collectionComponents . components
157199 : [ selectedComponent ] ;
158200
159201 setSelectedComponents ( ( prevSelectedComponents ) => {
@@ -166,49 +208,55 @@ export const ComponentPickerProvider = ({
166208
167209 const newSelectedComponents = [ ...prevSelectedComponents , ...newComponents ] ;
168210
169- // Handle collection selection (when selecting entire collection)
170- if ( Array . isArray ( collectionComponents ) && collectionComponents . length ) {
171- updateCollectionStatus (
172- selectedComponent . usageKey ,
173- collectionComponents . length ,
174- collectionComponents . length ,
175- ) ;
176- }
177-
178- // Handle individual component selection (with total count)
179- if ( typeof collectionComponents === 'number' ) {
180- const componentCollectionKeys = selectedComponent . collectionKeys ;
181- const selectedCollectionComponents = newSelectedComponents . filter (
182- ( component ) => component . collectionKeys ?. some (
183- ( key ) => componentCollectionKeys ?. includes ( key ) ,
184- ) ,
185- ) ;
186-
187- const collectionKey = findCommonCollectionKey (
188- componentCollectionKeys ,
189- selectedCollectionComponents ,
190- ) ;
191-
192- if ( collectionKey ) {
193- updateCollectionStatus (
194- collectionKey ,
195- selectedCollectionComponents . length ,
196- collectionComponents ,
211+ const collectionSizes = isCollectionSelection
212+ ? collectionComponents . affectedCollectionSizes
213+ : collectionComponents ;
214+
215+ if ( collectionSizes instanceof Map && collectionSizes . size > 0 ) {
216+ const selectedByCollection = new Map < string , number > ( ) ;
217+
218+ newSelectedComponents . forEach ( ( component ) => {
219+ component . collectionKeys ?. forEach ( ( key ) => {
220+ if ( collectionSizes . has ( key ) ) {
221+ selectedByCollection . set ( key , ( selectedByCollection . get ( key ) ?? 0 ) + 1 ) ;
222+ }
223+ } ) ;
224+ } ) ;
225+
226+ // Batch update all collection statuses
227+ setSelectedCollections ( ( prevSelectedCollections ) => {
228+ const collectionMap = new Map (
229+ prevSelectedCollections . map ( ( c ) => [ c . key , c ] ) ,
197230 ) ;
198- }
231+
232+ collectionSizes . forEach ( ( totalCount , collectionKey ) => {
233+ const selectedCount = selectedByCollection . get ( collectionKey ) ?? 0 ;
234+
235+ if ( selectedCount === 0 ) {
236+ collectionMap . delete ( collectionKey ) ;
237+ } else if ( selectedCount >= totalCount ) {
238+ collectionMap . set ( collectionKey , { key : collectionKey , status : 'selected' } ) ;
239+ } else {
240+ collectionMap . set ( collectionKey , { key : collectionKey , status : 'indeterminate' } ) ;
241+ }
242+ } ) ;
243+
244+ return Array . from ( collectionMap . values ( ) ) ;
245+ } ) ;
199246 }
200247
201248 onChangeComponentSelection ?.( newSelectedComponents ) ;
202249 return newSelectedComponents ;
203250 } ) ;
204- } , [ ] ) ;
251+ } , [ onChangeComponentSelection ] ) ;
205252
206253 const removeComponentFromSelectedComponents = useCallback < ComponentSelectedEvent > ( (
207254 selectedComponent : SelectedComponent ,
208- collectionComponents ?: SelectedComponent [ ] | number ,
255+ collectionComponents ?: CollectionData | Map < string , number > ,
209256 ) => {
210- const componentsToRemove = Array . isArray ( collectionComponents ) && collectionComponents . length
211- ? collectionComponents
257+ const isCollectionSelection = collectionComponents && 'components' in collectionComponents ;
258+ const componentsToRemove = isCollectionSelection
259+ ? collectionComponents . components
212260 : [ selectedComponent ] ;
213261 const usageKeysToRemove = new Set ( componentsToRemove . map ( ( c ) => c . usageKey ) ) ;
214262
@@ -217,33 +265,48 @@ export const ComponentPickerProvider = ({
217265 ( component ) => ! usageKeysToRemove . has ( component . usageKey ) ,
218266 ) ;
219267
220- if ( typeof collectionComponents === 'number' ) {
221- const componentCollectionKeys = selectedComponent . collectionKeys ;
222- const collectionKey = findCommonCollectionKey ( componentCollectionKeys , componentsToRemove ) ;
223-
224- if ( collectionKey ) {
225- const remainingCollectionComponents = newSelectedComponents . filter (
226- ( component ) => component . collectionKeys ?. includes ( collectionKey ) ,
268+ const collectionSizes = isCollectionSelection
269+ ? collectionComponents . affectedCollectionSizes
270+ : collectionComponents ;
271+
272+ if ( collectionSizes instanceof Map && collectionSizes . size > 0 ) {
273+ const selectedByCollection = new Map < string , number > ( ) ;
274+
275+ // Only count components for collections we care about
276+ newSelectedComponents . forEach ( ( component ) => {
277+ component . collectionKeys ?. forEach ( ( key ) => {
278+ if ( collectionSizes . has ( key ) ) {
279+ selectedByCollection . set ( key , ( selectedByCollection . get ( key ) ?? 0 ) + 1 ) ;
280+ }
281+ } ) ;
282+ } ) ;
283+
284+ // Batch update all collection statuses
285+ setSelectedCollections ( ( prevSelectedCollections ) => {
286+ const collectionMap = new Map (
287+ prevSelectedCollections . map ( ( c ) => [ c . key , c ] ) ,
227288 ) ;
228- updateCollectionStatus (
229- collectionKey ,
230- remainingCollectionComponents . length ,
231- collectionComponents ,
232- ) ;
233- }
234- } else {
235- // Fallback: remove collections that have no remaining components
236- setSelectedCollections ( ( prevSelectedCollections ) => prevSelectedCollections . filter (
237- ( collection ) => newSelectedComponents . some (
238- ( component ) => component . collectionKeys ?. includes ( collection . key ) ,
239- ) ,
240- ) ) ;
289+
290+ collectionSizes . forEach ( ( totalCount , collectionKey ) => {
291+ const selectedCount = selectedByCollection . get ( collectionKey ) ?? 0 ;
292+
293+ if ( selectedCount === 0 ) {
294+ collectionMap . delete ( collectionKey ) ;
295+ } else if ( selectedCount >= totalCount ) {
296+ collectionMap . set ( collectionKey , { key : collectionKey , status : 'selected' } ) ;
297+ } else {
298+ collectionMap . set ( collectionKey , { key : collectionKey , status : 'indeterminate' } ) ;
299+ }
300+ } ) ;
301+
302+ return Array . from ( collectionMap . values ( ) ) ;
303+ } ) ;
241304 }
242305
243306 onChangeComponentSelection ?.( newSelectedComponents ) ;
244307 return newSelectedComponents ;
245308 } ) ;
246- } , [ ] ) ;
309+ } , [ onChangeComponentSelection ] ) ;
247310
248311 const context = useMemo < ComponentPickerContextData > ( ( ) => {
249312 switch ( componentPickerMode ) {
0 commit comments