Skip to content

Commit d049ab5

Browse files
authored
feat(model-ad): add caching for pinned items in CT (MG-471) (Sage-Bionetworks#3687)
1 parent 6ad522e commit d049ab5

File tree

5 files changed

+138
-27
lines changed

5 files changed

+138
-27
lines changed

libs/explorers/services/src/lib/comparison-tool.service.spec.ts

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
22
import { ActivatedRoute, Router } from '@angular/router';
3-
import { ComparisonToolColumn } from '@sagebionetworks/explorers/models';
3+
import { ComparisonToolColumn, ComparisonToolConfig } from '@sagebionetworks/explorers/models';
44
import { mockComparisonToolDataConfig } from '@sagebionetworks/explorers/testing';
55
import { FilterService, MessageService } from 'primeng/api';
66
import { BehaviorSubject } from 'rxjs';
@@ -70,6 +70,113 @@ describe('ComparisonToolService', () => {
7070
expect(cachedColumns?.every((column) => column.selected)).toBe(true);
7171
});
7272

73+
describe('pinned items cache', () => {
74+
const mockConfigs: ComparisonToolConfig[] = [
75+
{
76+
...mockComparisonToolDataConfig[0],
77+
dropdowns: ['category1', 'option1'],
78+
},
79+
{
80+
...mockComparisonToolDataConfig[0],
81+
dropdowns: ['category1', 'option2'],
82+
},
83+
{
84+
...mockComparisonToolDataConfig[0],
85+
dropdowns: ['category2', 'option1'],
86+
},
87+
];
88+
89+
beforeEach(() => {
90+
injectService().initialize(mockConfigs, ['category1', 'option1']);
91+
});
92+
93+
it('should persist pinned items when switching between dropdown selections', () => {
94+
// Pin items for first selection
95+
service.pinItem('item1');
96+
service.pinItem('item2');
97+
expect(service.pinnedItems().size).toBe(2);
98+
expect(service.isPinned('item1')).toBe(true);
99+
expect(service.isPinned('item2')).toBe(true);
100+
101+
// Switch to second selection
102+
service.setDropdownSelection(['category1', 'option2']);
103+
// Should start with no pinned items for new selection
104+
expect(service.pinnedItems().size).toBe(0);
105+
expect(service.isPinned('item1')).toBe(false);
106+
107+
// Pin different items
108+
service.pinItem('item3');
109+
expect(service.pinnedItems().size).toBe(1);
110+
expect(service.isPinned('item3')).toBe(true);
111+
112+
// Switch back to first selection
113+
service.setDropdownSelection(['category1', 'option1']);
114+
// Should restore originally pinned items
115+
expect(service.pinnedItems().size).toBe(2);
116+
expect(service.isPinned('item1')).toBe(true);
117+
expect(service.isPinned('item2')).toBe(true);
118+
expect(service.isPinned('item3')).toBe(false);
119+
120+
// Switch to second selection again
121+
service.setDropdownSelection(['category1', 'option2']);
122+
// Should restore pinned items from second selection
123+
expect(service.pinnedItems().size).toBe(1);
124+
expect(service.isPinned('item3')).toBe(true);
125+
expect(service.isPinned('item1')).toBe(false);
126+
});
127+
128+
it('should handle unpinning items and persist changes', () => {
129+
// Pin and unpin items
130+
service.pinItem('item1');
131+
service.pinItem('item2');
132+
service.unpinItem('item1');
133+
expect(service.pinnedItems().size).toBe(1);
134+
expect(service.isPinned('item2')).toBe(true);
135+
136+
// Switch and come back
137+
service.setDropdownSelection(['category1', 'option2']);
138+
service.setDropdownSelection(['category1', 'option1']);
139+
140+
// Should remember that item1 was unpinned
141+
expect(service.pinnedItems().size).toBe(1);
142+
expect(service.isPinned('item1')).toBe(false);
143+
expect(service.isPinned('item2')).toBe(true);
144+
});
145+
146+
it('should handle toggling pins correctly', () => {
147+
service.togglePin('item1');
148+
expect(service.isPinned('item1')).toBe(true);
149+
150+
service.togglePin('item1');
151+
expect(service.isPinned('item1')).toBe(false);
152+
153+
// Pin again and switch selections
154+
service.togglePin('item1');
155+
expect(service.isPinned('item1')).toBe(true);
156+
157+
service.setDropdownSelection(['category1', 'option2']);
158+
service.setDropdownSelection(['category1', 'option1']);
159+
160+
// Should still be pinned after switching back
161+
expect(service.isPinned('item1')).toBe(true);
162+
});
163+
164+
it('should handle pinning multiple items at once', () => {
165+
service.pinList(['item1', 'item2', 'item3']);
166+
expect(service.pinnedItems().size).toBe(3);
167+
168+
// Switch and return
169+
service.setDropdownSelection(['category2', 'option1']);
170+
service.setDropdownSelection(['category1', 'option1']);
171+
172+
// Should restore all pinned items
173+
expect(service.pinnedItems().size).toBe(3);
174+
expect(service.isPinned('item1')).toBe(true);
175+
expect(service.isPinned('item2')).toBe(true);
176+
expect(service.isPinned('item3')).toBe(true);
177+
});
178+
});
179+
73180
describe('pin/unpin functionality', () => {
74181
it('should track pinned items correctly', () => {
75182
injectService().initialize(mockComparisonToolDataConfig);

libs/explorers/services/src/lib/comparison-tool.service.ts

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export class ComparisonToolService<T> {
6161
private readonly columnsForDropdownsSignal = signal<Map<string, ComparisonToolColumn[]>>(
6262
new Map(),
6363
);
64+
private readonly pinnedItemsForDropdownsSignal = signal<Map<string, Set<string>>>(new Map());
6465
private readonly unpinnedDataSignal = signal<T[]>([]);
6566
private readonly pinnedDataSignal = signal<T[]>([]);
6667

@@ -148,6 +149,11 @@ export class ComparisonToolService<T> {
148149
}
149150

150151
initialize(configs: ComparisonToolConfig[], selection?: string[]) {
152+
// if it is already initialized, do not re-initialize
153+
if (this.configs().length > 0) {
154+
return;
155+
}
156+
151157
this.configsSignal.set(configs ?? []);
152158
this.totalResultsCount.set(0);
153159
if (this.hasInitializedConfig) {
@@ -157,14 +163,8 @@ export class ComparisonToolService<T> {
157163
this.setUnpinnedData([]);
158164
this.setPinnedData([]);
159165

160-
if (!configs?.length) {
161-
this.updateDropdownSelectionIfChanged([]);
162-
this.columnsForDropdownsSignal.set(new Map());
163-
return;
164-
}
165-
166166
const normalizedSelection = this.normalizeSelection(selection ?? [], configs);
167-
this.updateDropdownSelectionIfChanged(normalizedSelection);
167+
this.dropdownSelectionSignal.set(normalizedSelection);
168168

169169
const columnsMap = new Map<string, ComparisonToolColumn[]>();
170170
for (const config of configs) {
@@ -175,14 +175,14 @@ export class ComparisonToolService<T> {
175175
}
176176

177177
this.columnsForDropdownsSignal.set(columnsMap);
178+
this.pinnedItemsForDropdownsSignal.set(new Map());
178179
this.hasInitializedConfig = true;
179180
}
180181

181182
setDropdownSelection(selection: string[]) {
182183
const configs = this.configsSignal();
183184
if (!configs.length) {
184-
const normalizedSelection = selection ?? [];
185-
this.updateDropdownSelectionIfChanged(normalizedSelection);
185+
this.updateDropdownSelectionIfChanged(selection ?? []);
186186
return;
187187
}
188188

@@ -339,9 +339,28 @@ export class ComparisonToolService<T> {
339339
return;
340340
}
341341

342+
// Save current pinned items before switching
343+
const previousSelection = this.dropdownSelectionSignal();
344+
const previousKey = this.dropdownKey(previousSelection);
345+
const currentPinnedItems = this.pinnedItemsSignal();
346+
347+
this.pinnedItemsForDropdownsSignal.update((cache) => {
348+
const next = new Map(cache);
349+
next.set(previousKey, new Set(currentPinnedItems));
350+
return next;
351+
});
352+
353+
// Switch to new selection
342354
this.dropdownSelectionSignal.set(selection);
343-
if (this.hasInitializedConfig) {
344-
this.resetPinnedItems();
355+
356+
// Restore pinned items for new selection
357+
const newKey = this.dropdownKey(selection);
358+
const cachedPinnedItems = this.pinnedItemsForDropdownsSignal().get(newKey);
359+
360+
if (cachedPinnedItems) {
361+
this.pinnedItemsSignal.set(new Set(cachedPinnedItems));
362+
} else {
363+
this.pinnedItemsSignal.set(new Set());
345364
}
346365
}
347366

libs/model-ad/disease-correlation-comparison-tool/src/lib/disease-correlation-comparison-tool.component.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,6 @@ export class DiseaseCorrelationComparisonToolComponent implements OnInit {
115115
}
116116

117117
getConfigs() {
118-
// Skip if already initialized (service persists at route level)
119-
if (this.comparisonToolService.configs().length > 0) {
120-
return;
121-
}
122-
123118
this.comparisonToolConfigService
124119
.getComparisonToolConfig(ComparisonToolPage.DiseaseCorrelation)
125120
.pipe(shareReplay(1), takeUntilDestroyed(this.destroyRef))

libs/model-ad/gene-expression-comparison-tool/src/lib/gene-expression-comparison-tool.component.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,6 @@ export class GeneExpressionComparisonToolComponent implements OnInit {
9797
}
9898

9999
getConfigs() {
100-
// Skip if already initialized (service persists at route level)
101-
if (this.comparisonToolService.configs().length > 0) {
102-
return;
103-
}
104-
105100
this.comparisonToolConfigService
106101
.getComparisonToolConfig(ComparisonToolPage.GeneExpression)
107102
.pipe(shareReplay(1), takeUntilDestroyed(this.destroyRef))

libs/model-ad/model-overview-comparison-tool/src/lib/model-overview-comparison-tool.component.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,6 @@ export class ModelOverviewComparisonToolComponent implements OnInit {
9292
}
9393

9494
getConfigs() {
95-
// Skip if already initialized (service persists at route level)
96-
if (this.comparisonToolService.configs().length > 0) {
97-
return;
98-
}
99-
10095
this.comparisonToolConfigService
10196
.getComparisonToolConfig(ComparisonToolPage.ModelOverview)
10297
.pipe(shareReplay(1), takeUntilDestroyed(this.destroyRef))

0 commit comments

Comments
 (0)