@@ -25,11 +25,26 @@ export interface FilterToggleEvent {
2525 action : 'show' | 'hide' ;
2626}
2727
28+ /**
29+ * Event emitted when an entity relationship toggle action is requested.
30+ * Components emit these events to expand/collapse entity relationships in the graph.
31+ * FilterStore instances subscribe and handle events for their scopeId.
32+ */
33+ export interface EntityRelationshipEvent {
34+ scopeId : string ;
35+ entityId : string ;
36+ action : 'show' | 'hide' ;
37+ }
38+
2839// Global event bus for filter toggle actions
2940const filterToggleEvents$ = new Subject < FilterToggleEvent > ( ) ;
3041
42+ // Global event bus for entity relationship toggle actions
43+ const entityRelationshipEvents$ = new Subject < EntityRelationshipEvent > ( ) ;
44+
3145// Store emitted events for testing purposes
3246const emittedFilterEvents : FilterToggleEvent [ ] = [ ] ;
47+ const emittedEntityRelationshipEvents : EntityRelationshipEvent [ ] = [ ] ;
3348
3449/**
3550 * Emit a filter toggle event. Any FilterStore listening for this scopeId
@@ -61,6 +76,40 @@ export const emitFilterToggle = (
6176 filterToggleEvents$ . next ( event ) ;
6277} ;
6378
79+ /**
80+ * Emit an entity relationship toggle event. Any FilterStore listening for this scopeId
81+ * will receive the event and update its expanded entity IDs state.
82+ *
83+ * @param scopeId - Unique identifier for the graph instance
84+ * @param entityId - The entity ID to expand/collapse
85+ * @param action - 'show' to expand, 'hide' to collapse
86+ */
87+ export const emitEntityRelationshipToggle = (
88+ scopeId : string ,
89+ entityId : string ,
90+ action : 'show' | 'hide'
91+ ) : void => {
92+ const event : EntityRelationshipEvent = { scopeId, entityId, action } ;
93+ emittedEntityRelationshipEvents . push ( event ) ;
94+ entityRelationshipEvents$ . next ( event ) ;
95+ } ;
96+
97+ /**
98+ * Check if an entity's relationships are expanded for the given scope.
99+ * Returns false gracefully if no store exists.
100+ *
101+ * @param scopeId - Unique identifier for the graph instance
102+ * @param entityId - The entity ID to check
103+ * @returns true if the entity's relationships are expanded
104+ */
105+ export const isEntityRelationshipExpandedForScope = (
106+ scopeId : string ,
107+ entityId : string
108+ ) : boolean => {
109+ const store = stores . get ( scopeId ) ;
110+ return store ?. isEntityRelationshipExpanded ( entityId ) ?? false ;
111+ } ;
112+
64113/**
65114 * Check if a filter is active for the given scope, field, and value.
66115 * Returns false gracefully if no store exists (no warning logged).
@@ -95,6 +144,20 @@ export const __clearEmittedFilterEvents = (): void => {
95144 emittedFilterEvents . length = 0 ;
96145} ;
97146
147+ /**
148+ * Get all emitted entity relationship events. Primarily for testing.
149+ */
150+ export const __getEmittedEntityRelationshipEvents = ( ) : EntityRelationshipEvent [ ] => {
151+ return [ ...emittedEntityRelationshipEvents ] ;
152+ } ;
153+
154+ /**
155+ * Clear all emitted entity relationship events. Primarily for testing.
156+ */
157+ export const __clearEmittedEntityRelationshipEvents = ( ) : void => {
158+ emittedEntityRelationshipEvents . length = 0 ;
159+ } ;
160+
98161// =============================================================================
99162// FilterStore Class
100163// =============================================================================
@@ -114,17 +177,26 @@ export class FilterStore {
114177 readonly scopeId : string ;
115178 private dataViewId ?: string ;
116179 private readonly filters$ = new BehaviorSubject < Filter [ ] > ( [ ] ) ;
117- private readonly eventSubscription : Subscription ;
180+ private readonly expandedEntityIds$ = new BehaviorSubject < Set < string > > ( new Set ( ) ) ;
181+ private readonly filterEventSubscription : Subscription ;
182+ private readonly entityRelationshipEventSubscription : Subscription ;
118183
119184 constructor ( scopeId : string ) {
120185 this . scopeId = scopeId ;
121186
122187 // Subscribe to filter toggle events for this scopeId
123- this . eventSubscription = filterToggleEvents$
188+ this . filterEventSubscription = filterToggleEvents$
124189 . pipe ( rxFilter ( ( event ) => event . scopeId === this . scopeId ) )
125190 . subscribe ( ( event ) => {
126191 this . toggleFilter ( event . field , event . value , event . action ) ;
127192 } ) ;
193+
194+ // Subscribe to entity relationship toggle events for this scopeId
195+ this . entityRelationshipEventSubscription = entityRelationshipEvents$
196+ . pipe ( rxFilter ( ( event ) => event . scopeId === this . scopeId ) )
197+ . subscribe ( ( event ) => {
198+ this . toggleEntityRelationship ( event . entityId , event . action ) ;
199+ } ) ;
128200 }
129201
130202 /**
@@ -183,20 +255,65 @@ export class FilterStore {
183255 return containsFilter ( this . filters$ . value , field , value ) ;
184256 }
185257
258+ // ===========================================================================
259+ // Entity Relationship State
260+ // ===========================================================================
261+
262+ /**
263+ * Toggle an entity's relationship expansion state.
264+ * @param entityId - The entity ID to expand/collapse
265+ * @param action - 'show' to expand, 'hide' to collapse
266+ */
267+ toggleEntityRelationship ( entityId : string , action : 'show' | 'hide' ) : void {
268+ const next = new Set ( this . expandedEntityIds$ . value ) ;
269+ if ( action === 'show' ) {
270+ next . add ( entityId ) ;
271+ } else {
272+ next . delete ( entityId ) ;
273+ }
274+ this . expandedEntityIds$ . next ( next ) ;
275+ }
276+
277+ /**
278+ * Check if an entity's relationships are currently expanded.
279+ */
280+ isEntityRelationshipExpanded ( entityId : string ) : boolean {
281+ return this . expandedEntityIds$ . value . has ( entityId ) ;
282+ }
283+
284+ /**
285+ * Get the current set of expanded entity IDs.
286+ */
287+ getExpandedEntityIds ( ) : Set < string > {
288+ return this . expandedEntityIds$ . value ;
289+ }
290+
291+ /**
292+ * Subscribe to expanded entity IDs changes.
293+ * @param callback - Function called when expanded entity IDs change
294+ * @returns Subscription that should be unsubscribed on cleanup
295+ */
296+ subscribeToExpandedEntityIds ( callback : ( expandedEntityIds : Set < string > ) => void ) : Subscription {
297+ return this . expandedEntityIds$ . subscribe ( callback ) ;
298+ }
299+
186300 /**
187301 * Reset the filter store to empty state.
188302 */
189303 reset ( ) : void {
190304 this . filters$ . next ( [ ] ) ;
305+ this . expandedEntityIds$ . next ( new Set ( ) ) ;
191306 }
192307
193308 /**
194- * Clean up the store by completing the BehaviorSubject and unsubscribing from events.
309+ * Clean up the store by completing the BehaviorSubjects and unsubscribing from events.
195310 * Called when the graph instance unmounts.
196311 */
197312 destroy ( ) : void {
198- this . eventSubscription . unsubscribe ( ) ;
313+ this . filterEventSubscription . unsubscribe ( ) ;
314+ this . entityRelationshipEventSubscription . unsubscribe ( ) ;
199315 this . filters$ . complete ( ) ;
316+ this . expandedEntityIds$ . complete ( ) ;
200317 }
201318}
202319
0 commit comments