Skip to content

Commit 2b1fce3

Browse files
Merge pull request #75 from mskilab-org/fix/filtered-events-custom-sort-filter
Fix/filtered events custom sort filter
2 parents ed01770 + fe9a433 commit 2b1fce3

File tree

6 files changed

+216
-55
lines changed

6 files changed

+216
-55
lines changed

src/components/filteredEventsListPanel/columnBuilders.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,17 +168,21 @@ export function buildColumnConfig(columnDef, records, rendererProps = {}, filter
168168
title,
169169
dataIndex,
170170
viewType = "string-basic",
171-
type = "string",
172171
width = 120,
173172
filterable = false,
174173
sortable = false,
175-
filterType = type === "numeric" ? "numeric" : type === "object" ? "object" : "string",
176174
filterSearch = false,
177175
ellipsis = false,
178176
rendererProps: columnRendererProps = {},
179177
fields,
180178
} = columnDef;
181179

180+
// Auto-detect type for class-icon viewType (expects object with {class, score, desc})
181+
const type = columnDef.type || (viewType === "class-icon" ? "object" : "string");
182+
183+
// Derive filterType from type if not explicitly specified
184+
const filterType = columnDef.filterType || (type === "numeric" ? "numeric" : type === "object" ? "object" : "string");
185+
182186
// Extract the t function from rendererProps (if provided by component)
183187
const { t, ...otherProps } = rendererProps;
184188

@@ -237,10 +241,7 @@ export function buildColumnConfig(columnDef, records, rendererProps = {}, filter
237241
columnConfig.filterSearch = true;
238242
}
239243
}
240-
// Apply controlled filteredValue if provided (enables programmatic filter reset)
241-
if (filteredValue !== null) {
242-
columnConfig.filteredValue = filteredValue;
243-
}
244+
columnConfig.filteredValue = filteredValue;
244245
}
245246

246247
// Add sorter if enabled

src/components/filteredEventsListPanel/index.js

Lines changed: 123 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Skeleton,
1515
Typography,
1616
Select,
17+
Checkbox,
1718
} from "antd";
1819
import * as d3 from "d3";
1920
import { roleColorMap, transitionStyle } from "../../helpers/utility";
@@ -29,7 +30,7 @@ import { buildColumnsFromSettings } from "./columnBuilders";
2930

3031
const { Text } = Typography;
3132

32-
const { selectFilteredEvent } = filteredEventsActions;
33+
const { selectFilteredEvent, setSelectedEventUids, toggleEventUidSelection, setColumnFilters, resetColumnFilters } = filteredEventsActions;
3334

3435
const EVENT_TYPES = ["all", "snv", "cna", "fusion", "complexsv"];
3536

@@ -48,37 +49,90 @@ const getColumnTitle = (title) => {
4849

4950
class FilteredEventsListPanel extends Component {
5051
handleResetFilters = () => {
51-
const { additionalColumns } = this.props;
52+
const { additionalColumns, resetColumnFilters } = this.props;
5253
const defaultColumnKeys = this.getDefaultColumnKeys();
5354
const additionalKeys = (additionalColumns || []).map((col) => col.key);
5455
const defaultKeys = [...new Set([...defaultColumnKeys, ...additionalKeys])];
56+
57+
resetColumnFilters();
5558
this.setState({
56-
geneFilters: [],
57-
tierFilters: [],
58-
typeFilters: [],
59-
roleFilters: [],
60-
effectFilters: [],
61-
variantFilters: [],
6259
selectedColumnKeys: defaultKeys,
6360
});
6461
};
62+
63+
handleCheckboxChange = (record, checked) => {
64+
const { toggleEventUidSelection } = this.props;
65+
toggleEventUidSelection(record.uid, checked);
66+
};
67+
68+
handleHeaderCheckboxChange = (records) => {
69+
const { selectedEventUids, setSelectedEventUids } = this.props;
70+
71+
// Get all tier 1 and 2 records from current view
72+
const tier1And2Records = records.filter(
73+
(r) => r.tier && (+r.tier === 1 || +r.tier === 2)
74+
);
75+
const tier1And2Uids = tier1And2Records.map((r) => r.uid);
76+
77+
// Check current state
78+
const selectedTier1And2 = tier1And2Uids.filter((uid) =>
79+
selectedEventUids.includes(uid)
80+
);
81+
const allSelected = selectedTier1And2.length === tier1And2Uids.length && tier1And2Uids.length > 0;
82+
83+
if (allSelected) {
84+
// Deselect all tier 1 and 2
85+
const newUids = selectedEventUids.filter(
86+
(uid) => !tier1And2Uids.includes(uid)
87+
);
88+
setSelectedEventUids(newUids);
89+
} else {
90+
// Select all tier 1 and 2
91+
const newSelectedUids = [...new Set([...selectedEventUids, ...tier1And2Uids])];
92+
setSelectedEventUids(newSelectedUids);
93+
}
94+
};
95+
96+
getHeaderCheckboxState = (records) => {
97+
const { selectedEventUids } = this.props;
98+
99+
// Get all tier 1 and 2 records from current view
100+
const tier1And2Records = records.filter(
101+
(r) => r.tier && (+r.tier === 1 || +r.tier === 2)
102+
);
103+
const tier1And2Uids = tier1And2Records.map((r) => r.uid);
104+
105+
if (tier1And2Uids.length === 0) {
106+
return { checked: false, indeterminate: false };
107+
}
108+
109+
const selectedTier1And2 = tier1And2Uids.filter((uid) =>
110+
selectedEventUids.includes(uid)
111+
);
112+
113+
if (selectedTier1And2.length === 0) {
114+
return { checked: false, indeterminate: false };
115+
} else if (selectedTier1And2.length === tier1And2Uids.length) {
116+
return { checked: true, indeterminate: false };
117+
} else {
118+
return { checked: false, indeterminate: true };
119+
}
120+
};
121+
122+
isEventSelected = (record) => {
123+
const { selectedEventUids } = this.props;
124+
return selectedEventUids.includes(record.uid);
125+
};
65126
state = {
66127
eventType: "all",
67-
tierFilters: [1, 2], // start with tiers 1 & 2 checked
68-
typeFilters: [],
69-
roleFilters: [],
70-
effectFilters: [],
71-
variantFilters: [],
72-
geneFilters: [],
73128
tierCountsMap: {},
129+
geneVariantsWithTierChanges: null,
74130
selectedColumnKeys: [],
75131
};
76132

77133
// Track if a fetch is in progress to prevent concurrent calls
78134
_isFetchingTierCounts = false;
79135

80-
// add as a class field
81-
82136
getDefaultColumnKeys = () => {
83137
const { data: settingsData, dataset } = this.props;
84138

@@ -150,16 +204,13 @@ class FilteredEventsListPanel extends Component {
150204
};
151205

152206
handleTableChange = (pagination, filters) => {
153-
// When the user changes filters (e.g. checks tier 3),
154-
// update tierFilters in the state:
155-
this.setState({
156-
geneFilters: filters.gene || [],
157-
tierFilters: filters.tier || [],
158-
typeFilters: filters.type || [],
159-
roleFilters: filters.role || [],
160-
effectFilters: filters.effect || [],
161-
variantFilters: filters.variant || [],
207+
const columnFilters = {};
208+
Object.keys(filters).forEach((key) => {
209+
if (filters[key] && filters[key].length > 0) {
210+
columnFilters[key] = filters[key];
211+
}
162212
});
213+
this.props.setColumnFilters(columnFilters);
163214
};
164215

165216
fetchTierCountsForRecords = async () => {
@@ -201,7 +252,7 @@ class FilteredEventsListPanel extends Component {
201252

202253
// If no gene-variants have tier changes, nothing to fetch
203254
if (geneVariantsWithTiers.size === 0) {
204-
this.setState({ tierCountsMap: {} });
255+
this.setState({ tierCountsMap: {}, geneVariantsWithTierChanges: geneVariantsWithTiers });
205256
return;
206257
}
207258

@@ -221,7 +272,7 @@ class FilteredEventsListPanel extends Component {
221272

222273
// Guard: nothing to fetch after filtering
223274
if (uniqueRecords.length === 0) {
224-
this.setState({ tierCountsMap: {} });
275+
this.setState({ tierCountsMap: {}, geneVariantsWithTierChanges: geneVariantsWithTiers });
225276
return;
226277
}
227278

@@ -248,15 +299,22 @@ class FilteredEventsListPanel extends Component {
248299
await Promise.all(batchPromises);
249300
}
250301

251-
this.setState({ tierCountsMap: map });
302+
this.setState({ tierCountsMap: map, geneVariantsWithTierChanges: geneVariantsWithTiers });
252303
} finally {
253304
this._isFetchingTierCounts = false;
254305
}
255306
};
256307

257308
getTierTooltipContent = (record) => {
258309
const key = `${record.gene}-${record.type}`;
259-
const tierCounts = this.state.tierCountsMap[key];
310+
const { tierCountsMap, geneVariantsWithTierChanges } = this.state;
311+
312+
// Check if this gene-variant has no tier changes
313+
if (geneVariantsWithTierChanges && !geneVariantsWithTierChanges.has(key)) {
314+
return "No tier change";
315+
}
316+
317+
const tierCounts = tierCountsMap[key];
260318
if (!tierCounts) return "Loading tier distribution...";
261319
const total =
262320
(tierCounts[1] || 0) + (tierCounts[2] || 0) + (tierCounts[3] || 0);
@@ -318,15 +376,8 @@ class FilteredEventsListPanel extends Component {
318376
let records =
319377
(eventType === "all" ? filteredEvents : recordsHash.get(eventType)) || [];
320378

321-
// Build filter values object for controlled filter state
322-
const filterValues = {
323-
gene: this.state.geneFilters,
324-
tier: this.state.tierFilters,
325-
type: this.state.typeFilters,
326-
role: this.state.roleFilters,
327-
effect: this.state.effectFilters,
328-
variant: this.state.variantFilters,
329-
};
379+
const { columnFilters } = this.props;
380+
const filterValues = { ...columnFilters };
330381

331382
// Build columns from settings.json and dataset configuration
332383
const columns = buildColumnsFromSettings(
@@ -341,6 +392,28 @@ class FilteredEventsListPanel extends Component {
341392
filterValues
342393
);
343394

395+
// Checkbox column for selecting events
396+
const headerCheckboxState = this.getHeaderCheckboxState(records);
397+
const checkboxColumn = {
398+
title: (
399+
<Checkbox
400+
checked={headerCheckboxState.checked}
401+
indeterminate={headerCheckboxState.indeterminate}
402+
onChange={() => this.handleHeaderCheckboxChange(records)}
403+
/>
404+
),
405+
key: "select",
406+
width: 50,
407+
fixed: "left",
408+
align: "center",
409+
render: (_, record) => (
410+
<Checkbox
411+
checked={this.isEventSelected(record)}
412+
onChange={(e) => this.handleCheckboxChange(record, e.target.checked)}
413+
/>
414+
),
415+
};
416+
344417
return (
345418
<Wrapper>
346419
{error ? (
@@ -459,9 +532,10 @@ class FilteredEventsListPanel extends Component {
459532
<Skeleton active loading={loading}>
460533
<Table
461534
columns={[
535+
checkboxColumn,
462536
...(additionalColumns || []),
463537
...columns,
464-
].filter((col) => selectedColumnKeys.includes(col.key))}
538+
].filter((col) => col.key === "select" || selectedColumnKeys.includes(col.key))}
465539
dataSource={records}
466540
pagination={{ pageSize: 50 }}
467541
showSorterTooltip={false}
@@ -621,6 +695,14 @@ FilteredEventsListPanel.defaultProps = {};
621695
const mapDispatchToProps = (dispatch) => ({
622696
selectFilteredEvent: (filteredEvent, viewMode) =>
623697
dispatch(selectFilteredEvent(filteredEvent, viewMode)),
698+
setSelectedEventUids: (uids) =>
699+
dispatch(setSelectedEventUids(uids)),
700+
toggleEventUidSelection: (uid, selected) =>
701+
dispatch(toggleEventUidSelection(uid, selected)),
702+
setColumnFilters: (columnFilters) =>
703+
dispatch(setColumnFilters(columnFilters)),
704+
resetColumnFilters: () =>
705+
dispatch(resetColumnFilters()),
624706
});
625707
const mapStateToProps = (state) => {
626708
const mergedEvents = selectMergedEvents(state);
@@ -630,6 +712,8 @@ const mapStateToProps = (state) => {
630712
filteredEvents: mergedEvents.filteredEvents,
631713
originalFilteredEvents: state.FilteredEvents.originalFilteredEvents,
632714
selectedFilteredEvent: mergedEvents.selectedFilteredEvent,
715+
selectedEventUids: state.FilteredEvents.selectedEventUids || [],
716+
columnFilters: state.FilteredEvents.columnFilters || { tier: [1, 2] },
633717
viewMode: state.FilteredEvents.viewMode,
634718
error: state.FilteredEvents.error,
635719
id: state.CaseReport.id,

src/components/reportButtonsPanel/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ class ReportButtonsPanel extends Component {
3131
};
3232

3333
handleExportNotes = async () => {
34-
const { mergedEvents } = this.props;
34+
const { mergedEvents, selectedEventUids } = this.props;
3535
try {
3636
this.setState({ exporting: true });
3737
const state = this.props;
38-
await exportReport(state, mergedEvents);
38+
await exportReport(state, mergedEvents, selectedEventUids);
3939
} catch (err) {
4040
console.error("Report export failed:", err);
4141
} finally {
@@ -44,11 +44,11 @@ class ReportButtonsPanel extends Component {
4444
};
4545

4646
handlePreviewReport = async () => {
47-
const { mergedEvents } = this.props;
47+
const { mergedEvents, selectedEventUids } = this.props;
4848
try {
4949
this.setState({ previewLoading: true, previewVisible: true });
5050
const state = this.props;
51-
const html = await previewReport(state, mergedEvents);
51+
const html = await previewReport(state, mergedEvents, selectedEventUids);
5252
this.setState({ previewHtml: html });
5353
} catch (err) {
5454
console.error("Report preview failed:", err);
@@ -202,6 +202,7 @@ const mapStateToProps = (state) => ({
202202
CaseReport: state.CaseReport,
203203
Interpretations: state.Interpretations,
204204
mergedEvents: require("../../redux/interpretations/selectors").selectMergedEvents(state),
205+
selectedEventUids: state.FilteredEvents.selectedEventUids || [],
205206
});
206207

207208
export default connect(

0 commit comments

Comments
 (0)