gramene-search 2.1.11 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/launch.json +11 -0
- package/.claude/settings.local.json +6 -1
- package/.env.example +6 -0
- package/.parcel-cache/13f2d5707e7af45c-RequestGraph +0 -0
- package/.parcel-cache/5ae0570a78c0dba3-AssetGraph +0 -0
- package/.parcel-cache/9ac092379278e465-BundleGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-13f2d5707e7af45c.txt +2 -2
- package/dist/index.js +2065 -154
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
- package/src/bundles/api.js +10 -4
- package/src/bundles/exprViz.js +97 -1
- package/src/bundles/index.js +4 -1
- package/src/bundles/ontologyEnrichment.js +14 -1
- package/src/bundles/savedViews.js +335 -0
- package/src/bundles/uiViewState.js +174 -0
- package/src/bundles/viewSnapshot.js +313 -0
- package/src/bundles/views.js +22 -0
- package/src/components/Auth.js +23 -3
- package/src/components/SaveView.js +157 -0
- package/src/components/exprViz/ExprVizView.js +16 -11
- package/src/components/exprViz/ParallelCoordsPlot.js +15 -0
- package/src/components/results/GeneList.js +45 -15
- package/src/components/results/OntologyEnrichment.js +13 -6
- package/src/components/results/details/BAR.js +148 -0
- package/src/components/results/details/Expression.js +43 -11
- package/src/components/results/details/Homology.js +170 -32
- package/src/components/results/details/Pathways.js +4 -2
- package/src/components/results/details/Sequences.js +24 -8
- package/src/demo.js +22 -17
- package/src/index.js +2 -1
- package/src/suppressDevWarnings.js +13 -0
- package/src/utils/bootView.js +38 -0
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ require("ag-grid-community/styles/ag-grid.css");
|
|
|
7
7
|
require("ag-grid-community/styles/ag-theme-quartz.css");
|
|
8
8
|
var $gXNCa$reduxbundler = require("redux-bundler");
|
|
9
9
|
var $gXNCa$lodash = require("lodash");
|
|
10
|
-
var $gXNCa$
|
|
11
|
-
var $gXNCa$
|
|
10
|
+
var $gXNCa$gramenebinsclientsrcbins = require("gramene-bins-client/src/bins");
|
|
11
|
+
var $gXNCa$gramenetreesclientsrctaxonomy = require("gramene-trees-client/src/taxonomy");
|
|
12
12
|
var $gXNCa$gramenetaxonomywithgenomes = require("gramene-taxonomy-with-genomes");
|
|
13
13
|
var $gXNCa$reactga4 = require("react-ga4");
|
|
14
14
|
var $gXNCa$moneyclip = require("money-clip");
|
|
@@ -17,8 +17,8 @@ var $gXNCa$reactswitch = require("react-switch");
|
|
|
17
17
|
var $gXNCa$reacticonsio5 = require("react-icons/io5");
|
|
18
18
|
var $gXNCa$reacticonsbs = require("react-icons/bs");
|
|
19
19
|
var $gXNCa$reacticonsgr = require("react-icons/gr");
|
|
20
|
-
var $gXNCa$grameneefpbrowser = require("gramene-efp-browser");
|
|
21
20
|
var $gXNCa$gramenegenetreevis = require("gramene-genetree-vis");
|
|
21
|
+
var $gXNCa$gramenetreesclientsrcgenetree = require("gramene-trees-client/src/genetree");
|
|
22
22
|
var $gXNCa$tbrowse = require("tbrowse");
|
|
23
23
|
var $gXNCa$reactdom = require("react-dom");
|
|
24
24
|
var $gXNCa$lodashkeyBy = require("lodash/keyBy");
|
|
@@ -222,6 +222,7 @@ $parcel$export(module.exports, "Filters", () => $693dd8c7a5607c3a$export$92a576f
|
|
|
222
222
|
$parcel$export(module.exports, "Results", () => $693dd8c7a5607c3a$export$4a34de49b46a2bfa);
|
|
223
223
|
$parcel$export(module.exports, "Views", () => $693dd8c7a5607c3a$export$5cb791131c501f6a);
|
|
224
224
|
$parcel$export(module.exports, "Auth", () => $c5d403787de8b05f$export$2e2bcd8739ae039);
|
|
225
|
+
$parcel$export(module.exports, "bootViewFromUrl", () => $c69c61828e36a328$export$2e2bcd8739ae039);
|
|
225
226
|
|
|
226
227
|
|
|
227
228
|
|
|
@@ -638,8 +639,8 @@ const $9d9aeaf9299e61a1$var$grameneTaxDist = {
|
|
|
638
639
|
grameneTaxonomy[tid].name = map.display_name;
|
|
639
640
|
});
|
|
640
641
|
const binnedResults = $9d9aeaf9299e61a1$var$formatFacetCountsForViz(grameneSearch.facet_counts.facet_fields.fixed_1000__bin);
|
|
641
|
-
let speciesTree = (0, ($parcel$interopDefault($gXNCa$
|
|
642
|
-
let binMapper = (0, ($parcel$interopDefault($gXNCa$
|
|
642
|
+
let speciesTree = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(grameneTaxonomy));
|
|
643
|
+
let binMapper = (0, ($parcel$interopDefault($gXNCa$gramenebinsclientsrcbins)))(grameneMaps);
|
|
643
644
|
let taxDist = (0, $gXNCa$gramenetaxonomywithgenomes.build)(binMapper, speciesTree);
|
|
644
645
|
taxDist.setBinType('fixed', 1000);
|
|
645
646
|
taxDist.setResults(binnedResults);
|
|
@@ -1549,6 +1550,26 @@ const $24971af0a229e0e3$var$grameneViews = {
|
|
|
1549
1550
|
newState = Object.assign({}, state);
|
|
1550
1551
|
newState.options.forEach((v)=>v.shouldScroll = false);
|
|
1551
1552
|
return newState;
|
|
1553
|
+
case 'GRAMENE_VIEWS_REPLACED':
|
|
1554
|
+
{
|
|
1555
|
+
// Snapshot restore. payload: { on: Set|Array<id>, touched: {id: true} }
|
|
1556
|
+
// Sets options[].show directly to 'on' or 'off' based on membership in
|
|
1557
|
+
// `on`. Leaves 'disabled' entries untouched (site config wins).
|
|
1558
|
+
const onSet = payload.on instanceof Set ? payload.on : new Set(payload.on || []);
|
|
1559
|
+
newState = Object.assign({}, state);
|
|
1560
|
+
newState.touched = {
|
|
1561
|
+
...payload.touched || {}
|
|
1562
|
+
};
|
|
1563
|
+
newState.options = state.options.map((v)=>{
|
|
1564
|
+
if (v.show === 'disabled') return v;
|
|
1565
|
+
return {
|
|
1566
|
+
...v,
|
|
1567
|
+
show: onSet.has(v.id) ? 'on' : 'off',
|
|
1568
|
+
shouldScroll: false
|
|
1569
|
+
};
|
|
1570
|
+
});
|
|
1571
|
+
return newState;
|
|
1572
|
+
}
|
|
1552
1573
|
default:
|
|
1553
1574
|
return state;
|
|
1554
1575
|
}
|
|
@@ -1572,6 +1593,19 @@ const $24971af0a229e0e3$var$grameneViews = {
|
|
|
1572
1593
|
payload: null
|
|
1573
1594
|
});
|
|
1574
1595
|
},
|
|
1596
|
+
// Snapshot restore. `on` is an array of view ids to switch on; everything
|
|
1597
|
+
// else (except disabled views) goes off. `touched` is the same map the
|
|
1598
|
+
// user-toggled view bundle maintains, so the auto-default logic in
|
|
1599
|
+
// selectGrameneViews respects what the user has explicitly set.
|
|
1600
|
+
doReplaceGrameneViews: ({ on: on, touched: touched })=>({ dispatch: dispatch })=>{
|
|
1601
|
+
dispatch({
|
|
1602
|
+
type: 'GRAMENE_VIEWS_REPLACED',
|
|
1603
|
+
payload: {
|
|
1604
|
+
on: on,
|
|
1605
|
+
touched: touched
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
},
|
|
1575
1609
|
selectRawGrameneViews: (state)=>state.grameneViews,
|
|
1576
1610
|
selectGrameneViews: (0, $gXNCa$reduxbundler.createSelector)('selectRawGrameneViews', 'selectConfiguration', 'selectGrameneSearch', 'selectGrameneFilters', (raw, config, search, filters)=>{
|
|
1577
1611
|
const overrides = config && config.views || null;
|
|
@@ -3913,8 +3947,12 @@ var $c921a0d2b34aadb6$export$2e2bcd8739ae039 = $c921a0d2b34aadb6$var$ontologies;
|
|
|
3913
3947
|
// activeTaxon: <taxon_id|null>,
|
|
3914
3948
|
// byTaxon: {
|
|
3915
3949
|
// [taxon_id]: {
|
|
3916
|
-
// selectedFields: [<solr field name>...],
|
|
3950
|
+
// selectedFields: [<solr field name>...], // order = column/axis order
|
|
3917
3951
|
// fieldsModalOpen: <bool>,
|
|
3952
|
+
// vizMode: 'heatmap'|'parallel', // persisted view config
|
|
3953
|
+
// scale: 'linear'|'log', // persisted view config
|
|
3954
|
+
// brushes: { [field]: [lo, hi] }, // persisted parallel-coords brushes
|
|
3955
|
+
// pendingLoad: <bool>, // transient: a restored view wants a re-fetch
|
|
3918
3956
|
// fetch: { status, offset, total, signature, requestId, error },
|
|
3919
3957
|
// rows: [<doc>...]
|
|
3920
3958
|
// }
|
|
@@ -3964,6 +4002,10 @@ const $4f15cd8a7d970b18$var$exprViz = {
|
|
|
3964
4002
|
return {
|
|
3965
4003
|
selectedFields: [],
|
|
3966
4004
|
fieldsModalOpen: false,
|
|
4005
|
+
vizMode: 'heatmap',
|
|
4006
|
+
scale: 'linear',
|
|
4007
|
+
brushes: {},
|
|
4008
|
+
pendingLoad: false,
|
|
3967
4009
|
fetch: {
|
|
3968
4010
|
status: 'idle',
|
|
3969
4011
|
offset: 0,
|
|
@@ -4046,6 +4088,9 @@ const $4f15cd8a7d970b18$var$exprViz = {
|
|
|
4046
4088
|
[payload.taxon]: {
|
|
4047
4089
|
...t,
|
|
4048
4090
|
selectedFields: payload.fields,
|
|
4091
|
+
// A new field selection invalidates any prior brushes (they
|
|
4092
|
+
// reference axes that may no longer exist) and the loaded rows.
|
|
4093
|
+
brushes: {},
|
|
4049
4094
|
rows: [],
|
|
4050
4095
|
fetch: {
|
|
4051
4096
|
status: 'idle',
|
|
@@ -4059,6 +4104,77 @@ const $4f15cd8a7d970b18$var$exprViz = {
|
|
|
4059
4104
|
}
|
|
4060
4105
|
};
|
|
4061
4106
|
}
|
|
4107
|
+
case 'EXPRVIZ_VIZMODE_SET':
|
|
4108
|
+
{
|
|
4109
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
4110
|
+
return {
|
|
4111
|
+
...state,
|
|
4112
|
+
byTaxon: {
|
|
4113
|
+
...state.byTaxon,
|
|
4114
|
+
[payload.taxon]: {
|
|
4115
|
+
...t,
|
|
4116
|
+
vizMode: payload.vizMode
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
};
|
|
4120
|
+
}
|
|
4121
|
+
case 'EXPRVIZ_SCALE_SET':
|
|
4122
|
+
{
|
|
4123
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
4124
|
+
return {
|
|
4125
|
+
...state,
|
|
4126
|
+
byTaxon: {
|
|
4127
|
+
...state.byTaxon,
|
|
4128
|
+
[payload.taxon]: {
|
|
4129
|
+
...t,
|
|
4130
|
+
scale: payload.scale
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
}
|
|
4135
|
+
case 'EXPRVIZ_BRUSHES_SET':
|
|
4136
|
+
{
|
|
4137
|
+
const t = ensureTaxon(state, payload.taxon);
|
|
4138
|
+
return {
|
|
4139
|
+
...state,
|
|
4140
|
+
byTaxon: {
|
|
4141
|
+
...state.byTaxon,
|
|
4142
|
+
[payload.taxon]: {
|
|
4143
|
+
...t,
|
|
4144
|
+
brushes: payload.brushes || {}
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
};
|
|
4148
|
+
}
|
|
4149
|
+
case 'EXPRVIZ_RESTORED':
|
|
4150
|
+
{
|
|
4151
|
+
// Re-apply persisted view config from a saved-view snapshot. Async
|
|
4152
|
+
// data (rows) is NOT in the snapshot — instead each taxon with a
|
|
4153
|
+
// field selection is flagged pendingLoad so reactExprVizRestoreLoad
|
|
4154
|
+
// re-fetches it once the search context is ready.
|
|
4155
|
+
const byTaxon = {
|
|
4156
|
+
...state.byTaxon
|
|
4157
|
+
};
|
|
4158
|
+
const cfg = payload.byTaxon || {};
|
|
4159
|
+
for (const tid of Object.keys(cfg)){
|
|
4160
|
+
const base = ensureTaxon(state, tid);
|
|
4161
|
+
const c = cfg[tid] || {};
|
|
4162
|
+
const selectedFields = Array.isArray(c.selectedFields) ? c.selectedFields : [];
|
|
4163
|
+
byTaxon[tid] = {
|
|
4164
|
+
...base,
|
|
4165
|
+
selectedFields: selectedFields,
|
|
4166
|
+
vizMode: c.vizMode || 'heatmap',
|
|
4167
|
+
scale: c.scale || 'linear',
|
|
4168
|
+
brushes: c.brushes || {},
|
|
4169
|
+
pendingLoad: selectedFields.length > 0
|
|
4170
|
+
};
|
|
4171
|
+
}
|
|
4172
|
+
return {
|
|
4173
|
+
...state,
|
|
4174
|
+
activeTaxon: payload.activeTaxon || state.activeTaxon,
|
|
4175
|
+
byTaxon: byTaxon
|
|
4176
|
+
};
|
|
4177
|
+
}
|
|
4062
4178
|
case 'EXPRVIZ_FIELDS_REORDERED':
|
|
4063
4179
|
{
|
|
4064
4180
|
const t = state.byTaxon[payload.taxon];
|
|
@@ -4089,6 +4205,7 @@ const $4f15cd8a7d970b18$var$exprViz = {
|
|
|
4089
4205
|
...state.byTaxon,
|
|
4090
4206
|
[payload.taxon]: {
|
|
4091
4207
|
...t,
|
|
4208
|
+
pendingLoad: false,
|
|
4092
4209
|
fetch: {
|
|
4093
4210
|
...t.fetch,
|
|
4094
4211
|
status: 'loading',
|
|
@@ -4346,6 +4463,41 @@ const $4f15cd8a7d970b18$var$exprViz = {
|
|
|
4346
4463
|
fields: fields
|
|
4347
4464
|
}
|
|
4348
4465
|
}),
|
|
4466
|
+
doSetExprVizVizMode: (taxon, vizMode)=>({ dispatch: dispatch })=>dispatch({
|
|
4467
|
+
type: 'EXPRVIZ_VIZMODE_SET',
|
|
4468
|
+
payload: {
|
|
4469
|
+
taxon: taxon,
|
|
4470
|
+
vizMode: vizMode
|
|
4471
|
+
}
|
|
4472
|
+
}),
|
|
4473
|
+
doSetExprVizScale: (taxon, scale)=>({ dispatch: dispatch })=>dispatch({
|
|
4474
|
+
type: 'EXPRVIZ_SCALE_SET',
|
|
4475
|
+
payload: {
|
|
4476
|
+
taxon: taxon,
|
|
4477
|
+
scale: scale
|
|
4478
|
+
}
|
|
4479
|
+
}),
|
|
4480
|
+
doSetExprVizBrushes: (taxon, brushes)=>({ dispatch: dispatch })=>dispatch({
|
|
4481
|
+
type: 'EXPRVIZ_BRUSHES_SET',
|
|
4482
|
+
payload: {
|
|
4483
|
+
taxon: taxon,
|
|
4484
|
+
brushes: brushes
|
|
4485
|
+
}
|
|
4486
|
+
}),
|
|
4487
|
+
// Re-apply persisted exprViz view config from a saved-view snapshot. Called
|
|
4488
|
+
// by viewSnapshot's doApplyViewSnapshot after filters/views have been
|
|
4489
|
+
// restored, so the re-fetch (driven by reactExprVizRestoreLoad) sees the
|
|
4490
|
+
// restored query context.
|
|
4491
|
+
doApplyExprVizSnapshot: (snap)=>({ dispatch: dispatch })=>{
|
|
4492
|
+
if (!snap || typeof snap !== 'object') return;
|
|
4493
|
+
dispatch({
|
|
4494
|
+
type: 'EXPRVIZ_RESTORED',
|
|
4495
|
+
payload: {
|
|
4496
|
+
activeTaxon: snap.activeTaxon || null,
|
|
4497
|
+
byTaxon: snap.byTaxon || {}
|
|
4498
|
+
}
|
|
4499
|
+
});
|
|
4500
|
+
},
|
|
4349
4501
|
doFetchExprVizData: (taxon)=>({ dispatch: dispatch, store: store })=>{
|
|
4350
4502
|
const ev = store.selectExprViz();
|
|
4351
4503
|
const t = ev.byTaxon[taxon];
|
|
@@ -4466,6 +4618,25 @@ const $4f15cd8a7d970b18$var$exprViz = {
|
|
|
4466
4618
|
actionCreator: 'doFetchExprVizPivot'
|
|
4467
4619
|
};
|
|
4468
4620
|
}),
|
|
4621
|
+
// After a saved view is applied, EXPRVIZ_RESTORED flags each taxon that had
|
|
4622
|
+
// a field selection with pendingLoad. Once the search context is live (not
|
|
4623
|
+
// 'init') and the view is on, re-fetch that taxon's rows. Fires one taxon at
|
|
4624
|
+
// a time; EXPRVIZ_FETCH_STARTED clears the flag so this won't loop. Robust to
|
|
4625
|
+
// a GRAMENE_SEARCH_CLEARED that wipes rows mid-restore — the flag persists.
|
|
4626
|
+
reactExprVizRestoreLoad: (0, $gXNCa$reduxbundler.createSelector)('selectExprViz', 'selectGrameneFiltersStatus', 'selectGrameneViews', (ev, filtersStatus, views)=>{
|
|
4627
|
+
if (!ev || filtersStatus === 'init') return;
|
|
4628
|
+
const onView = views && views.options && views.options.find((v)=>v.id === 'exprViz');
|
|
4629
|
+
if (!onView || onView.show !== 'on') return;
|
|
4630
|
+
for (const tid of Object.keys(ev.byTaxon || {})){
|
|
4631
|
+
const t = ev.byTaxon[tid];
|
|
4632
|
+
if (t && t.pendingLoad && t.selectedFields.length > 0 && t.rows.length === 0 && t.fetch.status !== 'loading') return {
|
|
4633
|
+
actionCreator: 'doFetchExprVizData',
|
|
4634
|
+
args: [
|
|
4635
|
+
tid
|
|
4636
|
+
]
|
|
4637
|
+
};
|
|
4638
|
+
}
|
|
4639
|
+
}),
|
|
4469
4640
|
selectExprViz: (state)=>state.exprViz,
|
|
4470
4641
|
selectExprVizPivot: (state)=>state.exprViz.pivot,
|
|
4471
4642
|
selectExprVizActiveTaxon: (state)=>state.exprViz.activeTaxon,
|
|
@@ -4569,7 +4740,10 @@ const $d365d8c287ab0c94$var$ontologyEnrichment = {
|
|
|
4569
4740
|
maxGSSize: 500,
|
|
4570
4741
|
mostSpecific: false,
|
|
4571
4742
|
ontology: 'all',
|
|
4572
|
-
search: ''
|
|
4743
|
+
search: '',
|
|
4744
|
+
// Per-section table sort, keyed by ontology section id (e.g.
|
|
4745
|
+
// 'GO:biological_process'): { [sectionKey]: { key, dir } }.
|
|
4746
|
+
sort: {}
|
|
4573
4747
|
}
|
|
4574
4748
|
};
|
|
4575
4749
|
function ensureTaxon(state, tid) {
|
|
@@ -4773,6 +4947,21 @@ const $d365d8c287ab0c94$var$ontologyEnrichment = {
|
|
|
4773
4947
|
type: 'ONTOLOGY_ENRICHMENT_UI_SET',
|
|
4774
4948
|
payload: patch
|
|
4775
4949
|
}),
|
|
4950
|
+
// Re-apply persisted view config from a saved-view snapshot. Setting the
|
|
4951
|
+
// active taxon (last) lets reactOntologyEnrichmentFetch re-fetch the
|
|
4952
|
+
// foreground/background facets for the restored filters — no bulk data is
|
|
4953
|
+
// carried in the snapshot.
|
|
4954
|
+
doApplyOntologyEnrichmentSnapshot: (snap)=>({ dispatch: dispatch })=>{
|
|
4955
|
+
if (!snap || typeof snap !== 'object') return;
|
|
4956
|
+
if (snap.ui) dispatch({
|
|
4957
|
+
type: 'ONTOLOGY_ENRICHMENT_UI_SET',
|
|
4958
|
+
payload: snap.ui
|
|
4959
|
+
});
|
|
4960
|
+
if (snap.activeTaxon) dispatch({
|
|
4961
|
+
type: 'ONTOLOGY_ENRICHMENT_ACTIVE_TAXON_SET',
|
|
4962
|
+
payload: snap.activeTaxon
|
|
4963
|
+
});
|
|
4964
|
+
},
|
|
4776
4965
|
doFetchOntologyEnrichmentForeground: (taxon)=>({ dispatch: dispatch, store: store })=>{
|
|
4777
4966
|
const q = store.selectGrameneFiltersQueryString();
|
|
4778
4967
|
const signature = $d365d8c287ab0c94$var$fgSig(q, taxon);
|
|
@@ -5130,6 +5319,1009 @@ function $d365d8c287ab0c94$var$collapseToMostSpecific(ontKey, rows, recs) {
|
|
|
5130
5319
|
var $d365d8c287ab0c94$export$2e2bcd8739ae039 = $d365d8c287ab0c94$var$ontologyEnrichment;
|
|
5131
5320
|
|
|
5132
5321
|
|
|
5322
|
+
// Per-gene UI state lifted out of <Gene> and <Homology> class components.
|
|
5323
|
+
// Keyed by geneId so it survives unmount/remount (e.g. scrolling a row out
|
|
5324
|
+
// of view and back), and so a snapshot serializer can later round-trip it
|
|
5325
|
+
// for the shareable-views feature.
|
|
5326
|
+
//
|
|
5327
|
+
// What lives here:
|
|
5328
|
+
// - expandedDetail: which detail tab is open in the gene-list card
|
|
5329
|
+
// - fullscreen: whether the expanded detail is full-screened
|
|
5330
|
+
// - homology.viewer: 'treevis' | 'tbrowse'
|
|
5331
|
+
// - homology.height: drag-resize height of the homology detail pane
|
|
5332
|
+
// - homology.tbrowse: the tbrowse ViewState (collapsedNodeIds, prunedNodeIds,
|
|
5333
|
+
// swappedNodeIds, compressedNodeIds, nodeOfInterestId, zones, zoneStates,
|
|
5334
|
+
// search, selectedNodeId). Tbrowse is driven in controlled mode from this.
|
|
5335
|
+
// - sequences: the Sequences detail's internal state — tab ('dna'|'rna'|'pep'),
|
|
5336
|
+
// tid (selected transcript id), upstream/downstream (flanking bp). Driven in
|
|
5337
|
+
// controlled mode from this so a saved view restores the chosen sub-tab and
|
|
5338
|
+
// isoform.
|
|
5339
|
+
// - expression: the Expression detail's internal state — activeTab
|
|
5340
|
+
// ('gene'|'paralogs'|'eFP'), atlasExperiment (selected GXA experiment id),
|
|
5341
|
+
// barStudy (selected eFP/BAR study). Driven in controlled mode from this so a
|
|
5342
|
+
// saved view restores the chosen sub-tab and study.
|
|
5343
|
+
//
|
|
5344
|
+
// What does NOT live here:
|
|
5345
|
+
// - derived/computed state like `details` (per-render config from
|
|
5346
|
+
// props.config.details + capabilities)
|
|
5347
|
+
// - async-fetched data caches in <Homology> (neighborhood, geneStructures)
|
|
5348
|
+
const $7f865ea0feda21af$var$initialState = {
|
|
5349
|
+
byGene: {}
|
|
5350
|
+
};
|
|
5351
|
+
const $7f865ea0feda21af$var$ensureGene = (state, geneId)=>{
|
|
5352
|
+
if (state.byGene[geneId]) return state;
|
|
5353
|
+
return {
|
|
5354
|
+
...state,
|
|
5355
|
+
byGene: {
|
|
5356
|
+
...state.byGene,
|
|
5357
|
+
[geneId]: {
|
|
5358
|
+
homology: {}
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
};
|
|
5362
|
+
};
|
|
5363
|
+
const $7f865ea0feda21af$var$setGene = (state, geneId, patch)=>{
|
|
5364
|
+
const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
|
|
5365
|
+
return {
|
|
5366
|
+
...next,
|
|
5367
|
+
byGene: {
|
|
5368
|
+
...next.byGene,
|
|
5369
|
+
[geneId]: {
|
|
5370
|
+
...next.byGene[geneId],
|
|
5371
|
+
...patch
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
};
|
|
5375
|
+
};
|
|
5376
|
+
const $7f865ea0feda21af$var$setHomology = (state, geneId, patch)=>{
|
|
5377
|
+
const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
|
|
5378
|
+
const prev = next.byGene[geneId];
|
|
5379
|
+
return {
|
|
5380
|
+
...next,
|
|
5381
|
+
byGene: {
|
|
5382
|
+
...next.byGene,
|
|
5383
|
+
[geneId]: {
|
|
5384
|
+
...prev,
|
|
5385
|
+
homology: {
|
|
5386
|
+
...prev.homology || {},
|
|
5387
|
+
...patch
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
};
|
|
5392
|
+
};
|
|
5393
|
+
const $7f865ea0feda21af$var$setSequences = (state, geneId, patch)=>{
|
|
5394
|
+
const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
|
|
5395
|
+
const prev = next.byGene[geneId];
|
|
5396
|
+
return {
|
|
5397
|
+
...next,
|
|
5398
|
+
byGene: {
|
|
5399
|
+
...next.byGene,
|
|
5400
|
+
[geneId]: {
|
|
5401
|
+
...prev,
|
|
5402
|
+
sequences: {
|
|
5403
|
+
...prev.sequences || {},
|
|
5404
|
+
...patch
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
};
|
|
5409
|
+
};
|
|
5410
|
+
const $7f865ea0feda21af$var$setExpression = (state, geneId, patch)=>{
|
|
5411
|
+
const next = $7f865ea0feda21af$var$ensureGene(state, geneId);
|
|
5412
|
+
const prev = next.byGene[geneId];
|
|
5413
|
+
return {
|
|
5414
|
+
...next,
|
|
5415
|
+
byGene: {
|
|
5416
|
+
...next.byGene,
|
|
5417
|
+
[geneId]: {
|
|
5418
|
+
...prev,
|
|
5419
|
+
expression: {
|
|
5420
|
+
...prev.expression || {},
|
|
5421
|
+
...patch
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
};
|
|
5426
|
+
};
|
|
5427
|
+
const $7f865ea0feda21af$var$uiViewState = {
|
|
5428
|
+
name: 'uiViewState',
|
|
5429
|
+
getReducer: ()=>(state = $7f865ea0feda21af$var$initialState, { type: type, payload: payload })=>{
|
|
5430
|
+
switch(type){
|
|
5431
|
+
case 'UI_GENE_DETAIL_EXPANDED':
|
|
5432
|
+
// payload: { geneId, detail } (detail = null to collapse)
|
|
5433
|
+
return $7f865ea0feda21af$var$setGene(state, payload.geneId, {
|
|
5434
|
+
expandedDetail: payload.detail,
|
|
5435
|
+
// collapsing also exits fullscreen
|
|
5436
|
+
fullscreen: payload.detail === null ? false : state.byGene[payload.geneId]?.fullscreen || false
|
|
5437
|
+
});
|
|
5438
|
+
case 'UI_GENE_FULLSCREEN_SET':
|
|
5439
|
+
// payload: { geneId, fullscreen }
|
|
5440
|
+
return $7f865ea0feda21af$var$setGene(state, payload.geneId, {
|
|
5441
|
+
fullscreen: !!payload.fullscreen
|
|
5442
|
+
});
|
|
5443
|
+
case 'UI_HOMOLOGY_VIEWER_SET':
|
|
5444
|
+
// payload: { geneId, viewer }
|
|
5445
|
+
return $7f865ea0feda21af$var$setHomology(state, payload.geneId, {
|
|
5446
|
+
viewer: payload.viewer
|
|
5447
|
+
});
|
|
5448
|
+
case 'UI_HOMOLOGY_HEIGHT_SET':
|
|
5449
|
+
// payload: { geneId, height }
|
|
5450
|
+
return $7f865ea0feda21af$var$setHomology(state, payload.geneId, {
|
|
5451
|
+
height: payload.height
|
|
5452
|
+
});
|
|
5453
|
+
case 'UI_HOMOLOGY_TBROWSE_SET':
|
|
5454
|
+
// payload: { geneId, tbrowse } -- a full ViewState object
|
|
5455
|
+
return $7f865ea0feda21af$var$setHomology(state, payload.geneId, {
|
|
5456
|
+
tbrowse: payload.tbrowse
|
|
5457
|
+
});
|
|
5458
|
+
case 'UI_SEQUENCES_SET':
|
|
5459
|
+
// payload: { geneId, patch } -- merges into the sequences slice
|
|
5460
|
+
// (e.g. { tab }, { tid }, { upstream }, { downstream })
|
|
5461
|
+
return $7f865ea0feda21af$var$setSequences(state, payload.geneId, payload.patch);
|
|
5462
|
+
case 'UI_EXPRESSION_SET':
|
|
5463
|
+
// payload: { geneId, patch } -- merges into the expression slice
|
|
5464
|
+
// (e.g. { activeTab }, { atlasExperiment }, { barStudy })
|
|
5465
|
+
return $7f865ea0feda21af$var$setExpression(state, payload.geneId, payload.patch);
|
|
5466
|
+
case 'UI_VIEW_STATE_REPLACED':
|
|
5467
|
+
// payload: { byGene } -- used by the snapshot loader in a later phase
|
|
5468
|
+
return {
|
|
5469
|
+
byGene: payload.byGene || {}
|
|
5470
|
+
};
|
|
5471
|
+
default:
|
|
5472
|
+
return state;
|
|
5473
|
+
}
|
|
5474
|
+
},
|
|
5475
|
+
selectUiViewState: (state)=>state.uiViewState,
|
|
5476
|
+
doExpandGeneDetail: ({ geneId: geneId, detail: detail })=>({ dispatch: dispatch })=>{
|
|
5477
|
+
dispatch({
|
|
5478
|
+
type: 'UI_GENE_DETAIL_EXPANDED',
|
|
5479
|
+
payload: {
|
|
5480
|
+
geneId: geneId,
|
|
5481
|
+
detail: detail
|
|
5482
|
+
}
|
|
5483
|
+
});
|
|
5484
|
+
},
|
|
5485
|
+
doSetGeneFullscreen: ({ geneId: geneId, fullscreen: fullscreen })=>({ dispatch: dispatch })=>{
|
|
5486
|
+
dispatch({
|
|
5487
|
+
type: 'UI_GENE_FULLSCREEN_SET',
|
|
5488
|
+
payload: {
|
|
5489
|
+
geneId: geneId,
|
|
5490
|
+
fullscreen: fullscreen
|
|
5491
|
+
}
|
|
5492
|
+
});
|
|
5493
|
+
},
|
|
5494
|
+
doSetHomologyViewer: ({ geneId: geneId, viewer: viewer })=>({ dispatch: dispatch })=>{
|
|
5495
|
+
dispatch({
|
|
5496
|
+
type: 'UI_HOMOLOGY_VIEWER_SET',
|
|
5497
|
+
payload: {
|
|
5498
|
+
geneId: geneId,
|
|
5499
|
+
viewer: viewer
|
|
5500
|
+
}
|
|
5501
|
+
});
|
|
5502
|
+
},
|
|
5503
|
+
doSetHomologyHeight: ({ geneId: geneId, height: height })=>({ dispatch: dispatch })=>{
|
|
5504
|
+
dispatch({
|
|
5505
|
+
type: 'UI_HOMOLOGY_HEIGHT_SET',
|
|
5506
|
+
payload: {
|
|
5507
|
+
geneId: geneId,
|
|
5508
|
+
height: height
|
|
5509
|
+
}
|
|
5510
|
+
});
|
|
5511
|
+
},
|
|
5512
|
+
doSetHomologyTbrowseViewState: ({ geneId: geneId, tbrowse: tbrowse })=>({ dispatch: dispatch })=>{
|
|
5513
|
+
dispatch({
|
|
5514
|
+
type: 'UI_HOMOLOGY_TBROWSE_SET',
|
|
5515
|
+
payload: {
|
|
5516
|
+
geneId: geneId,
|
|
5517
|
+
tbrowse: tbrowse
|
|
5518
|
+
}
|
|
5519
|
+
});
|
|
5520
|
+
},
|
|
5521
|
+
doSetSequencesState: ({ geneId: geneId, patch: patch })=>({ dispatch: dispatch })=>{
|
|
5522
|
+
dispatch({
|
|
5523
|
+
type: 'UI_SEQUENCES_SET',
|
|
5524
|
+
payload: {
|
|
5525
|
+
geneId: geneId,
|
|
5526
|
+
patch: patch
|
|
5527
|
+
}
|
|
5528
|
+
});
|
|
5529
|
+
},
|
|
5530
|
+
doSetExpressionState: ({ geneId: geneId, patch: patch })=>({ dispatch: dispatch })=>{
|
|
5531
|
+
dispatch({
|
|
5532
|
+
type: 'UI_EXPRESSION_SET',
|
|
5533
|
+
payload: {
|
|
5534
|
+
geneId: geneId,
|
|
5535
|
+
patch: patch
|
|
5536
|
+
}
|
|
5537
|
+
});
|
|
5538
|
+
},
|
|
5539
|
+
doReplaceUiViewState: (byGene)=>({ dispatch: dispatch })=>{
|
|
5540
|
+
dispatch({
|
|
5541
|
+
type: 'UI_VIEW_STATE_REPLACED',
|
|
5542
|
+
payload: {
|
|
5543
|
+
byGene: byGene
|
|
5544
|
+
}
|
|
5545
|
+
});
|
|
5546
|
+
}
|
|
5547
|
+
};
|
|
5548
|
+
var $7f865ea0feda21af$export$2e2bcd8739ae039 = $7f865ea0feda21af$var$uiViewState;
|
|
5549
|
+
|
|
5550
|
+
|
|
5551
|
+
// Serializer/deserializer for shareable gene-search views.
|
|
5552
|
+
//
|
|
5553
|
+
// `selectViewSnapshot` produces a versioned JSON-safe blob from the current
|
|
5554
|
+
// store state. `doApplyViewSnapshot` validates a blob and restores it via
|
|
5555
|
+
// existing action creators. Persistence + UI live in later phases — this
|
|
5556
|
+
// bundle is the pure state-shape layer they build on.
|
|
5557
|
+
//
|
|
5558
|
+
// Snapshot schema, v1:
|
|
5559
|
+
// {
|
|
5560
|
+
// v: 1,
|
|
5561
|
+
// capturedAt: <ISO 8601 string>,
|
|
5562
|
+
// site: <subsite id>,
|
|
5563
|
+
// filters: <grameneFilters tree, stripped of ephemeral UI flags>,
|
|
5564
|
+
// views: { on: [id,...], touched: {id: true, ...} },
|
|
5565
|
+
// genomeSubset: { taxonId: true, ... } | null,
|
|
5566
|
+
// searchPage: { offset: number, rows: number } | null,
|
|
5567
|
+
// exprViz: { // Expression-visualization view
|
|
5568
|
+
// activeTaxon: <taxon_id|null>,
|
|
5569
|
+
// byTaxon: { [taxon_id]: { selectedFields: [field,...], vizMode, scale, brushes } }
|
|
5570
|
+
// } | undefined, // loaded rows re-fetch on apply
|
|
5571
|
+
// ontologyEnrichment: { // Ontology Enrichment view
|
|
5572
|
+
// activeTaxon: <taxon_id|null>,
|
|
5573
|
+
// ui: { pAdjCutoff, minGSSize, maxGSSize, mostSpecific, ontology, search, sort }
|
|
5574
|
+
// } | undefined, // facets re-fetch on apply
|
|
5575
|
+
// expandedDetails: [
|
|
5576
|
+
// { geneId,
|
|
5577
|
+
// expandedDetail: string|null,
|
|
5578
|
+
// fullscreen: boolean,
|
|
5579
|
+
// homology: { viewer, height, tbrowse: <ViewState> } | undefined,
|
|
5580
|
+
// sequences: { tab, tid, upstream, downstream } | undefined,
|
|
5581
|
+
// expression: { activeTab, atlasExperiment, barStudy } | undefined
|
|
5582
|
+
// }, ...
|
|
5583
|
+
// ]
|
|
5584
|
+
// }
|
|
5585
|
+
//
|
|
5586
|
+
// Anything not in this schema is intentionally NOT persisted — async fetch
|
|
5587
|
+
// caches, search-results, pagination cursors mid-flight, etc. all rehydrate
|
|
5588
|
+
// from the API once the snapshot is applied.
|
|
5589
|
+
const $472fe3745b238881$var$SCHEMA_VERSION = 1;
|
|
5590
|
+
// Keys on a filter node we want to keep. Everything else (showMenu, marked,
|
|
5591
|
+
// status flags from the root, etc.) is ephemeral UI and not meaningful in a
|
|
5592
|
+
// shared view.
|
|
5593
|
+
const $472fe3745b238881$var$FILTER_NODE_KEEP = [
|
|
5594
|
+
'leftIdx',
|
|
5595
|
+
'rightIdx',
|
|
5596
|
+
'operation',
|
|
5597
|
+
'negate',
|
|
5598
|
+
'fq_field',
|
|
5599
|
+
'fq_value',
|
|
5600
|
+
'name',
|
|
5601
|
+
'category',
|
|
5602
|
+
'warning'
|
|
5603
|
+
];
|
|
5604
|
+
const $472fe3745b238881$var$FILTER_ROOT_KEEP = [
|
|
5605
|
+
'operation',
|
|
5606
|
+
'negate',
|
|
5607
|
+
'leftIdx',
|
|
5608
|
+
'rightIdx',
|
|
5609
|
+
'children',
|
|
5610
|
+
'searchOffset',
|
|
5611
|
+
'rows'
|
|
5612
|
+
];
|
|
5613
|
+
const $472fe3745b238881$var$cleanFilterNode = (node)=>{
|
|
5614
|
+
const out = {};
|
|
5615
|
+
for (const k of $472fe3745b238881$var$FILTER_NODE_KEEP)if (node[k] !== undefined) out[k] = node[k];
|
|
5616
|
+
if (Array.isArray(node.children)) out.children = node.children.map($472fe3745b238881$var$cleanFilterNode);
|
|
5617
|
+
return out;
|
|
5618
|
+
};
|
|
5619
|
+
const $472fe3745b238881$var$cleanFilters = (filters)=>{
|
|
5620
|
+
const out = {};
|
|
5621
|
+
for (const k of $472fe3745b238881$var$FILTER_ROOT_KEEP)if (filters[k] !== undefined) out[k] = filters[k];
|
|
5622
|
+
if (Array.isArray(filters.children)) out.children = filters.children.map($472fe3745b238881$var$cleanFilterNode);
|
|
5623
|
+
return out;
|
|
5624
|
+
};
|
|
5625
|
+
// Drop bundle entries that hold no meaningful state (e.g. a row the user
|
|
5626
|
+
// scrolled past but never opened). Otherwise snapshots bloat with empties.
|
|
5627
|
+
const $472fe3745b238881$var$isMeaningfulGene = (entry)=>{
|
|
5628
|
+
if (!entry) return false;
|
|
5629
|
+
if (entry.expandedDetail) return true;
|
|
5630
|
+
if (entry.fullscreen) return true;
|
|
5631
|
+
if (entry.homology && (entry.homology.viewer !== undefined || entry.homology.height !== undefined || entry.homology.tbrowse !== undefined)) return true;
|
|
5632
|
+
if (entry.sequences && Object.keys(entry.sequences).length) return true;
|
|
5633
|
+
if (entry.expression && Object.keys(entry.expression).length) return true;
|
|
5634
|
+
return false;
|
|
5635
|
+
};
|
|
5636
|
+
const $472fe3745b238881$var$viewSnapshot = {
|
|
5637
|
+
name: 'viewSnapshot',
|
|
5638
|
+
// No reducer — this bundle is pure derivation + dispatch orchestration.
|
|
5639
|
+
getReducer: ()=>(state = {})=>state,
|
|
5640
|
+
selectViewSnapshot: $472fe3745b238881$var$createSnapshotSelector(),
|
|
5641
|
+
// Apply a snapshot to the store. Returns an array of unresolved-id warnings
|
|
5642
|
+
// so the caller can surface a non-blocking notice. The action creators we
|
|
5643
|
+
// delegate to are existing ones; nothing here knows about API/UI/share-link
|
|
5644
|
+
// — that's later phases.
|
|
5645
|
+
doApplyViewSnapshot: (snapshot)=>({ dispatch: dispatch, store: store })=>{
|
|
5646
|
+
const warnings = $472fe3745b238881$var$validateSnapshot(snapshot);
|
|
5647
|
+
if (warnings.fatal) {
|
|
5648
|
+
console.warn('viewSnapshot: refusing to apply', warnings.fatal);
|
|
5649
|
+
return {
|
|
5650
|
+
applied: false,
|
|
5651
|
+
warnings: warnings
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
// 1. Filters (also clears search; replays a fresh fetch).
|
|
5655
|
+
if (snapshot.filters) dispatch({
|
|
5656
|
+
type: 'BATCH_ACTIONS',
|
|
5657
|
+
actions: [
|
|
5658
|
+
{
|
|
5659
|
+
type: 'GRAMENE_SEARCH_CLEARED'
|
|
5660
|
+
},
|
|
5661
|
+
{
|
|
5662
|
+
type: 'GRAMENE_FILTERS_REPLACED',
|
|
5663
|
+
payload: $472fe3745b238881$var$cloneForDispatch(snapshot.filters)
|
|
5664
|
+
}
|
|
5665
|
+
]
|
|
5666
|
+
});
|
|
5667
|
+
// 2. Views.
|
|
5668
|
+
if (snapshot.views) dispatch({
|
|
5669
|
+
type: 'GRAMENE_VIEWS_REPLACED',
|
|
5670
|
+
payload: {
|
|
5671
|
+
on: snapshot.views.on || [],
|
|
5672
|
+
touched: snapshot.views.touched || {}
|
|
5673
|
+
}
|
|
5674
|
+
});
|
|
5675
|
+
// 3. Genome subset, if present.
|
|
5676
|
+
if (snapshot.genomeSubset && Object.keys(snapshot.genomeSubset).length) dispatch({
|
|
5677
|
+
type: 'GRAMENE_GENOMES_UPDATED',
|
|
5678
|
+
payload: {
|
|
5679
|
+
...snapshot.genomeSubset
|
|
5680
|
+
}
|
|
5681
|
+
});
|
|
5682
|
+
// 4. Per-gene UI state (expandedDetail, fullscreen, homology viewer/
|
|
5683
|
+
// height, tbrowse view state). Replace wholesale via the uiViewState
|
|
5684
|
+
// bundle's bulk action.
|
|
5685
|
+
const byGene = {};
|
|
5686
|
+
if (Array.isArray(snapshot.expandedDetails)) for (const e of snapshot.expandedDetails){
|
|
5687
|
+
if (!e || !e.geneId) continue;
|
|
5688
|
+
byGene[e.geneId] = {
|
|
5689
|
+
expandedDetail: e.expandedDetail || null,
|
|
5690
|
+
fullscreen: !!e.fullscreen,
|
|
5691
|
+
homology: e.homology ? {
|
|
5692
|
+
...e.homology
|
|
5693
|
+
} : {},
|
|
5694
|
+
sequences: e.sequences ? {
|
|
5695
|
+
...e.sequences
|
|
5696
|
+
} : {},
|
|
5697
|
+
expression: e.expression ? {
|
|
5698
|
+
...e.expression
|
|
5699
|
+
} : {}
|
|
5700
|
+
};
|
|
5701
|
+
}
|
|
5702
|
+
dispatch({
|
|
5703
|
+
type: 'UI_VIEW_STATE_REPLACED',
|
|
5704
|
+
payload: {
|
|
5705
|
+
byGene: byGene
|
|
5706
|
+
}
|
|
5707
|
+
});
|
|
5708
|
+
// 5. Expression-visualization view config. Delegate to the exprViz bundle,
|
|
5709
|
+
// which flags each taxon for a re-fetch now that the restored filters/
|
|
5710
|
+
// genomes are in place. Done last so the re-fetch sees the live query.
|
|
5711
|
+
if (snapshot.exprViz && store.doApplyExprVizSnapshot) store.doApplyExprVizSnapshot(snapshot.exprViz);
|
|
5712
|
+
// 6. Ontology Enrichment view config. Setting the active taxon lets that
|
|
5713
|
+
// bundle's fetch reactor re-derive the enrichment for the restored
|
|
5714
|
+
// filters/genomes.
|
|
5715
|
+
if (snapshot.ontologyEnrichment && store.doApplyOntologyEnrichmentSnapshot) store.doApplyOntologyEnrichmentSnapshot(snapshot.ontologyEnrichment);
|
|
5716
|
+
return {
|
|
5717
|
+
applied: true,
|
|
5718
|
+
warnings: warnings
|
|
5719
|
+
};
|
|
5720
|
+
},
|
|
5721
|
+
// Helper exposed for the bootstrap path that wants to read warnings without
|
|
5722
|
+
// dispatching. Returns { fatal: <string|null>, unresolved: { ids/taxa/views/genes lists } }.
|
|
5723
|
+
doValidateViewSnapshot: (snapshot)=>()=>$472fe3745b238881$var$validateSnapshot(snapshot)
|
|
5724
|
+
};
|
|
5725
|
+
function $472fe3745b238881$var$createSnapshotSelector() {
|
|
5726
|
+
// We deliberately don't memoize with createSelector — snapshots are taken
|
|
5727
|
+
// on demand (Save button click, link generation) rather than on every
|
|
5728
|
+
// store tick, so a fresh build is fine and avoids stale-dep bugs.
|
|
5729
|
+
return (state)=>$472fe3745b238881$var$buildSnapshot(state);
|
|
5730
|
+
}
|
|
5731
|
+
function $472fe3745b238881$var$buildSnapshot(state) {
|
|
5732
|
+
const snap = {
|
|
5733
|
+
v: $472fe3745b238881$var$SCHEMA_VERSION,
|
|
5734
|
+
capturedAt: new Date().toISOString(),
|
|
5735
|
+
site: state.config && state.config.id || null
|
|
5736
|
+
};
|
|
5737
|
+
if (state.grameneFilters) {
|
|
5738
|
+
snap.filters = $472fe3745b238881$var$cleanFilters(state.grameneFilters);
|
|
5739
|
+
snap.searchPage = {
|
|
5740
|
+
offset: state.grameneFilters.searchOffset || 0,
|
|
5741
|
+
rows: state.grameneFilters.rows || 20
|
|
5742
|
+
};
|
|
5743
|
+
}
|
|
5744
|
+
if (state.grameneViews && Array.isArray(state.grameneViews.options)) snap.views = {
|
|
5745
|
+
on: state.grameneViews.options.filter((v)=>v && v.show === 'on').map((v)=>v.id),
|
|
5746
|
+
touched: {
|
|
5747
|
+
...state.grameneViews.touched || {}
|
|
5748
|
+
}
|
|
5749
|
+
};
|
|
5750
|
+
if (state.grameneGenomes && state.grameneGenomes.active) {
|
|
5751
|
+
const keys = Object.keys(state.grameneGenomes.active);
|
|
5752
|
+
if (keys.length) {
|
|
5753
|
+
snap.genomeSubset = {};
|
|
5754
|
+
for (const k of keys)snap.genomeSubset[k] = true;
|
|
5755
|
+
} else snap.genomeSubset = null;
|
|
5756
|
+
}
|
|
5757
|
+
if (state.uiViewState && state.uiViewState.byGene) snap.expandedDetails = Object.entries(state.uiViewState.byGene).filter(([, e])=>$472fe3745b238881$var$isMeaningfulGene(e)).map(([geneId, e])=>({
|
|
5758
|
+
geneId: geneId,
|
|
5759
|
+
expandedDetail: e.expandedDetail || null,
|
|
5760
|
+
fullscreen: !!e.fullscreen,
|
|
5761
|
+
homology: e.homology && Object.keys(e.homology).length ? {
|
|
5762
|
+
...e.homology
|
|
5763
|
+
} : undefined,
|
|
5764
|
+
sequences: e.sequences && Object.keys(e.sequences).length ? {
|
|
5765
|
+
...e.sequences
|
|
5766
|
+
} : undefined,
|
|
5767
|
+
expression: e.expression && Object.keys(e.expression).length ? {
|
|
5768
|
+
...e.expression
|
|
5769
|
+
} : undefined
|
|
5770
|
+
}));
|
|
5771
|
+
else snap.expandedDetails = [];
|
|
5772
|
+
// Expression-visualization view config. Capture only the selection/config
|
|
5773
|
+
// (active genome tab, per-taxon selected fields + column order, viz mode,
|
|
5774
|
+
// scale, brush ranges) — never the bulk fetched rows, which rehydrate via
|
|
5775
|
+
// the view's re-fetch on apply.
|
|
5776
|
+
if (state.exprViz) {
|
|
5777
|
+
const ev = state.exprViz;
|
|
5778
|
+
const byTaxon = {};
|
|
5779
|
+
for (const tid of Object.keys(ev.byTaxon || {})){
|
|
5780
|
+
const t = ev.byTaxon[tid];
|
|
5781
|
+
if (!t) continue;
|
|
5782
|
+
const selectedFields = Array.isArray(t.selectedFields) ? t.selectedFields : [];
|
|
5783
|
+
const brushes = t.brushes && Object.keys(t.brushes).length ? {
|
|
5784
|
+
...t.brushes
|
|
5785
|
+
} : undefined;
|
|
5786
|
+
const nonDefaultMode = t.vizMode && t.vizMode !== 'heatmap';
|
|
5787
|
+
const nonDefaultScale = t.scale && t.scale !== 'linear';
|
|
5788
|
+
if (selectedFields.length || brushes || nonDefaultMode || nonDefaultScale) byTaxon[tid] = {
|
|
5789
|
+
selectedFields: selectedFields,
|
|
5790
|
+
vizMode: t.vizMode || 'heatmap',
|
|
5791
|
+
scale: t.scale || 'linear',
|
|
5792
|
+
...brushes ? {
|
|
5793
|
+
brushes: brushes
|
|
5794
|
+
} : {}
|
|
5795
|
+
};
|
|
5796
|
+
}
|
|
5797
|
+
if (ev.activeTaxon || Object.keys(byTaxon).length) snap.exprViz = {
|
|
5798
|
+
activeTaxon: ev.activeTaxon || null,
|
|
5799
|
+
byTaxon: byTaxon
|
|
5800
|
+
};
|
|
5801
|
+
}
|
|
5802
|
+
// Ontology Enrichment view config: active genome + the analysis knobs
|
|
5803
|
+
// (significance cutoff, gene-set-size bounds, most-specific, ontology
|
|
5804
|
+
// section, term search, per-section sort). The enrichment facets re-fetch
|
|
5805
|
+
// on apply, so no bulk term data is carried.
|
|
5806
|
+
if (state.ontologyEnrichment) {
|
|
5807
|
+
const oe = state.ontologyEnrichment;
|
|
5808
|
+
const ui = oe.ui || {};
|
|
5809
|
+
const DEF = {
|
|
5810
|
+
pAdjCutoff: 0.05,
|
|
5811
|
+
minGSSize: 10,
|
|
5812
|
+
maxGSSize: 500,
|
|
5813
|
+
mostSpecific: false,
|
|
5814
|
+
ontology: 'all',
|
|
5815
|
+
search: ''
|
|
5816
|
+
};
|
|
5817
|
+
const hasSort = ui.sort && Object.keys(ui.sort).length;
|
|
5818
|
+
const uiChanged = Object.keys(DEF).some((k)=>ui[k] !== DEF[k]) || hasSort;
|
|
5819
|
+
if (oe.activeTaxon || uiChanged) snap.ontologyEnrichment = {
|
|
5820
|
+
activeTaxon: oe.activeTaxon || null,
|
|
5821
|
+
ui: {
|
|
5822
|
+
pAdjCutoff: ui.pAdjCutoff != null ? ui.pAdjCutoff : DEF.pAdjCutoff,
|
|
5823
|
+
minGSSize: ui.minGSSize != null ? ui.minGSSize : DEF.minGSSize,
|
|
5824
|
+
maxGSSize: ui.maxGSSize != null ? ui.maxGSSize : DEF.maxGSSize,
|
|
5825
|
+
mostSpecific: !!ui.mostSpecific,
|
|
5826
|
+
ontology: ui.ontology || DEF.ontology,
|
|
5827
|
+
search: ui.search || DEF.search,
|
|
5828
|
+
...hasSort ? {
|
|
5829
|
+
sort: {
|
|
5830
|
+
...ui.sort
|
|
5831
|
+
}
|
|
5832
|
+
} : {}
|
|
5833
|
+
}
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5836
|
+
return snap;
|
|
5837
|
+
}
|
|
5838
|
+
function $472fe3745b238881$var$validateSnapshot(snapshot) {
|
|
5839
|
+
const warnings = {
|
|
5840
|
+
fatal: null,
|
|
5841
|
+
unresolved: {
|
|
5842
|
+
views: [],
|
|
5843
|
+
genes: [],
|
|
5844
|
+
taxa: []
|
|
5845
|
+
}
|
|
5846
|
+
};
|
|
5847
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
5848
|
+
warnings.fatal = 'snapshot is not an object';
|
|
5849
|
+
return warnings;
|
|
5850
|
+
}
|
|
5851
|
+
if (snapshot.v !== $472fe3745b238881$var$SCHEMA_VERSION) {
|
|
5852
|
+
warnings.fatal = `unsupported snapshot version ${snapshot.v} (expected ${$472fe3745b238881$var$SCHEMA_VERSION})`;
|
|
5853
|
+
return warnings;
|
|
5854
|
+
}
|
|
5855
|
+
// Strict-id checks happen later — they need the store's grameneMaps,
|
|
5856
|
+
// grameneTaxonomy, etc. which are async-loaded. The boot path will call
|
|
5857
|
+
// doValidateViewSnapshot again after those land. For now we just
|
|
5858
|
+
// structurally validate.
|
|
5859
|
+
return warnings;
|
|
5860
|
+
}
|
|
5861
|
+
// JSON.parse(JSON.stringify(x)) — bundles see a wholly fresh object so
|
|
5862
|
+
// reducers can't accidentally mutate the snapshot we're holding for retry.
|
|
5863
|
+
function $472fe3745b238881$var$cloneForDispatch(x) {
|
|
5864
|
+
return JSON.parse(JSON.stringify(x));
|
|
5865
|
+
}
|
|
5866
|
+
var $472fe3745b238881$export$2e2bcd8739ae039 = $472fe3745b238881$var$viewSnapshot;
|
|
5867
|
+
|
|
5868
|
+
|
|
5869
|
+
// Boot-time hydration of a shared view link.
|
|
5870
|
+
//
|
|
5871
|
+
// Entry points call `bootViewFromUrl(store)` once the store is created.
|
|
5872
|
+
// Public views resolve anonymously. For private views, the initial call
|
|
5873
|
+
// will 401; the Auth panel then re-invokes with `{user}` once Firebase has
|
|
5874
|
+
// emitted its first signed-in state, and the fetch retries with a Bearer
|
|
5875
|
+
// token. On success we apply the snapshot and strip `?view=` from the URL
|
|
5876
|
+
// so subsequent user actions don't sit under a stale shared-state URL.
|
|
5877
|
+
//
|
|
5878
|
+
// All errors are non-fatal — they surface in state.savedViews.fetchError
|
|
5879
|
+
// for any UI that wants to show them.
|
|
5880
|
+
const $c69c61828e36a328$var$PARAM = 'view';
|
|
5881
|
+
function $c69c61828e36a328$export$2e2bcd8739ae039(store, opts = {}) {
|
|
5882
|
+
if (typeof window === 'undefined') return Promise.resolve(null);
|
|
5883
|
+
const url = new URL(window.location.href);
|
|
5884
|
+
const hash = url.searchParams.get($c69c61828e36a328$var$PARAM);
|
|
5885
|
+
if (!hash) return Promise.resolve(null);
|
|
5886
|
+
const { user: user = null } = opts;
|
|
5887
|
+
return store.doFetchView({
|
|
5888
|
+
hash: hash,
|
|
5889
|
+
user: user
|
|
5890
|
+
}).then(({ snapshot: snapshot })=>{
|
|
5891
|
+
store.doApplyViewSnapshot(snapshot);
|
|
5892
|
+
url.searchParams.delete($c69c61828e36a328$var$PARAM);
|
|
5893
|
+
window.history.replaceState({}, '', url.toString());
|
|
5894
|
+
return {
|
|
5895
|
+
hash: hash,
|
|
5896
|
+
applied: true
|
|
5897
|
+
};
|
|
5898
|
+
}).catch((err)=>{
|
|
5899
|
+
// 401 here on the anonymous pass is expected for private views — the
|
|
5900
|
+
// Auth panel will retry once it has a user. Other errors (404, 5xx,
|
|
5901
|
+
// network) are reported and leave the param so a manual refresh can
|
|
5902
|
+
// retry too.
|
|
5903
|
+
console.warn('bootViewFromUrl:', err.message || err);
|
|
5904
|
+
return {
|
|
5905
|
+
hash: hash,
|
|
5906
|
+
applied: false,
|
|
5907
|
+
error: err.message || String(err)
|
|
5908
|
+
};
|
|
5909
|
+
});
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5912
|
+
|
|
5913
|
+
// Client for the saved-views API ({api}/saved_views — see gramene-swagger
|
|
5914
|
+
// phase 3b). Mirrors the gene_lists pattern: Firebase Bearer ID token,
|
|
5915
|
+
// hash-based addressing, public vs private scope.
|
|
5916
|
+
//
|
|
5917
|
+
// Action creators all return Promises so calling UI (modal, boot path,
|
|
5918
|
+
// list view) can await them and surface success/error inline.
|
|
5919
|
+
//
|
|
5920
|
+
// Dev-mode mock: set `window.__SAVED_VIEWS_MOCK__ = true` in the console
|
|
5921
|
+
// and saves/fetches go through localStorage instead of the network. Lets us
|
|
5922
|
+
// drive Phase 4 (UI) without waiting on Phase 3b (server). Production
|
|
5923
|
+
// builds never touch the mock unless the flag is set at runtime, so this
|
|
5924
|
+
// is safe to ship.
|
|
5925
|
+
const $85a9f6732ceb79db$var$STORAGE_PREFIX = 'gramene_saved_view_mock_v1::';
|
|
5926
|
+
const $85a9f6732ceb79db$var$initialState = {
|
|
5927
|
+
saving: false,
|
|
5928
|
+
saveError: null,
|
|
5929
|
+
lastSavedHash: null,
|
|
5930
|
+
fetching: false,
|
|
5931
|
+
fetchError: null,
|
|
5932
|
+
lastFetched: null,
|
|
5933
|
+
privateList: null,
|
|
5934
|
+
publicList: null,
|
|
5935
|
+
listError: null
|
|
5936
|
+
};
|
|
5937
|
+
const $85a9f6732ceb79db$var$savedViews = {
|
|
5938
|
+
name: 'savedViews',
|
|
5939
|
+
getReducer: ()=>(state = $85a9f6732ceb79db$var$initialState, { type: type, payload: payload })=>{
|
|
5940
|
+
switch(type){
|
|
5941
|
+
case 'SAVED_VIEW_SAVE_STARTED':
|
|
5942
|
+
return {
|
|
5943
|
+
...state,
|
|
5944
|
+
saving: true,
|
|
5945
|
+
saveError: null
|
|
5946
|
+
};
|
|
5947
|
+
case 'SAVED_VIEW_SAVE_SUCCEEDED':
|
|
5948
|
+
return {
|
|
5949
|
+
...state,
|
|
5950
|
+
saving: false,
|
|
5951
|
+
lastSavedHash: payload.hash
|
|
5952
|
+
};
|
|
5953
|
+
case 'SAVED_VIEW_SAVE_FAILED':
|
|
5954
|
+
return {
|
|
5955
|
+
...state,
|
|
5956
|
+
saving: false,
|
|
5957
|
+
saveError: payload.error
|
|
5958
|
+
};
|
|
5959
|
+
case 'SAVED_VIEW_FETCH_STARTED':
|
|
5960
|
+
return {
|
|
5961
|
+
...state,
|
|
5962
|
+
fetching: true,
|
|
5963
|
+
fetchError: null
|
|
5964
|
+
};
|
|
5965
|
+
case 'SAVED_VIEW_FETCH_SUCCEEDED':
|
|
5966
|
+
return {
|
|
5967
|
+
...state,
|
|
5968
|
+
fetching: false,
|
|
5969
|
+
lastFetched: payload
|
|
5970
|
+
};
|
|
5971
|
+
case 'SAVED_VIEW_FETCH_FAILED':
|
|
5972
|
+
return {
|
|
5973
|
+
...state,
|
|
5974
|
+
fetching: false,
|
|
5975
|
+
fetchError: payload.error
|
|
5976
|
+
};
|
|
5977
|
+
case 'SAVED_VIEW_LIST_RECEIVED':
|
|
5978
|
+
return {
|
|
5979
|
+
...state,
|
|
5980
|
+
[payload.kind === 'public' ? 'publicList' : 'privateList']: payload.rows,
|
|
5981
|
+
listError: null
|
|
5982
|
+
};
|
|
5983
|
+
case 'SAVED_VIEW_LIST_FAILED':
|
|
5984
|
+
return {
|
|
5985
|
+
...state,
|
|
5986
|
+
listError: payload.error
|
|
5987
|
+
};
|
|
5988
|
+
case 'SAVED_VIEW_RESET':
|
|
5989
|
+
return {
|
|
5990
|
+
...$85a9f6732ceb79db$var$initialState
|
|
5991
|
+
};
|
|
5992
|
+
default:
|
|
5993
|
+
return state;
|
|
5994
|
+
}
|
|
5995
|
+
},
|
|
5996
|
+
selectSavedViews: (state)=>state.savedViews,
|
|
5997
|
+
// POST a snapshot. Returns Promise<{hash, shareUrl}> on success.
|
|
5998
|
+
// `user` is a Firebase user object (must respond to .getIdToken()).
|
|
5999
|
+
doSaveView: ({ user: user, label: label, description: description, isPublic: isPublic })=>async ({ dispatch: dispatch, store: store })=>{
|
|
6000
|
+
dispatch({
|
|
6001
|
+
type: 'SAVED_VIEW_SAVE_STARTED'
|
|
6002
|
+
});
|
|
6003
|
+
try {
|
|
6004
|
+
const snapshot = store.selectViewSnapshot();
|
|
6005
|
+
const hash = await $85a9f6732ceb79db$var$computeContentHash(snapshot);
|
|
6006
|
+
const site = store.selectConfiguration && store.selectConfiguration().id || '';
|
|
6007
|
+
const token = user && user.getIdToken ? await user.getIdToken() : null;
|
|
6008
|
+
if (!token) throw new Error('Not signed in');
|
|
6009
|
+
const meta = {
|
|
6010
|
+
hash: hash,
|
|
6011
|
+
label: label,
|
|
6012
|
+
site: site,
|
|
6013
|
+
isPublic: !!isPublic,
|
|
6014
|
+
description: description || ''
|
|
6015
|
+
};
|
|
6016
|
+
if ($85a9f6732ceb79db$var$useMock()) $85a9f6732ceb79db$var$mockSave({
|
|
6017
|
+
...meta,
|
|
6018
|
+
state: snapshot,
|
|
6019
|
+
uid: user.uid || 'mock',
|
|
6020
|
+
createdAt: new Date().toISOString()
|
|
6021
|
+
});
|
|
6022
|
+
else {
|
|
6023
|
+
const api = store.selectGrameneAPI();
|
|
6024
|
+
const res = await fetch(`${api}/saved_views`, {
|
|
6025
|
+
method: 'POST',
|
|
6026
|
+
headers: {
|
|
6027
|
+
'Content-Type': 'application/json',
|
|
6028
|
+
Authorization: `Bearer ${token}`
|
|
6029
|
+
},
|
|
6030
|
+
body: JSON.stringify({
|
|
6031
|
+
...meta,
|
|
6032
|
+
state: snapshot
|
|
6033
|
+
})
|
|
6034
|
+
});
|
|
6035
|
+
if (!res.ok) throw new Error(`Save failed (${res.status})`);
|
|
6036
|
+
}
|
|
6037
|
+
const shareUrl = $85a9f6732ceb79db$var$buildShareUrl(hash);
|
|
6038
|
+
dispatch({
|
|
6039
|
+
type: 'SAVED_VIEW_SAVE_SUCCEEDED',
|
|
6040
|
+
payload: {
|
|
6041
|
+
hash: hash
|
|
6042
|
+
}
|
|
6043
|
+
});
|
|
6044
|
+
return {
|
|
6045
|
+
hash: hash,
|
|
6046
|
+
shareUrl: shareUrl
|
|
6047
|
+
};
|
|
6048
|
+
} catch (err) {
|
|
6049
|
+
dispatch({
|
|
6050
|
+
type: 'SAVED_VIEW_SAVE_FAILED',
|
|
6051
|
+
payload: {
|
|
6052
|
+
error: err.message || String(err)
|
|
6053
|
+
}
|
|
6054
|
+
});
|
|
6055
|
+
throw err;
|
|
6056
|
+
}
|
|
6057
|
+
},
|
|
6058
|
+
// GET a snapshot by share hash. Anonymous-OK for public views; token is
|
|
6059
|
+
// optional and only sent if the caller provides a user.
|
|
6060
|
+
// Returns Promise<{snapshot, meta}>.
|
|
6061
|
+
doFetchView: ({ hash: hash, user: user })=>async ({ dispatch: dispatch, store: store })=>{
|
|
6062
|
+
dispatch({
|
|
6063
|
+
type: 'SAVED_VIEW_FETCH_STARTED'
|
|
6064
|
+
});
|
|
6065
|
+
try {
|
|
6066
|
+
let row;
|
|
6067
|
+
if ($85a9f6732ceb79db$var$useMock()) {
|
|
6068
|
+
row = $85a9f6732ceb79db$var$mockFetch(hash);
|
|
6069
|
+
if (!row) throw new Error(`No saved view with hash ${hash}`);
|
|
6070
|
+
} else {
|
|
6071
|
+
const api = store.selectGrameneAPI();
|
|
6072
|
+
const headers = {
|
|
6073
|
+
Accept: 'application/json'
|
|
6074
|
+
};
|
|
6075
|
+
if (user && user.getIdToken) try {
|
|
6076
|
+
headers.Authorization = `Bearer ${await user.getIdToken()}`;
|
|
6077
|
+
} catch (_) {}
|
|
6078
|
+
const res = await fetch(`${api}/saved_views?hash=${encodeURIComponent(hash)}`, {
|
|
6079
|
+
headers: headers
|
|
6080
|
+
});
|
|
6081
|
+
if (res.status === 401) throw new Error('Sign in to load this private view.');
|
|
6082
|
+
if (res.status === 404) throw new Error('Saved view not found (it may have been deleted).');
|
|
6083
|
+
if (!res.ok) throw new Error(`Fetch failed (${res.status})`);
|
|
6084
|
+
row = await res.json();
|
|
6085
|
+
}
|
|
6086
|
+
const out = {
|
|
6087
|
+
hash: hash,
|
|
6088
|
+
snapshot: row.state,
|
|
6089
|
+
meta: {
|
|
6090
|
+
label: row.label,
|
|
6091
|
+
description: row.description || '',
|
|
6092
|
+
site: row.site,
|
|
6093
|
+
isPublic: !!row.isPublic,
|
|
6094
|
+
owner: row.owner || null,
|
|
6095
|
+
createdAt: row.createdAt || null
|
|
6096
|
+
}
|
|
6097
|
+
};
|
|
6098
|
+
dispatch({
|
|
6099
|
+
type: 'SAVED_VIEW_FETCH_SUCCEEDED',
|
|
6100
|
+
payload: out
|
|
6101
|
+
});
|
|
6102
|
+
return out;
|
|
6103
|
+
} catch (err) {
|
|
6104
|
+
dispatch({
|
|
6105
|
+
type: 'SAVED_VIEW_FETCH_FAILED',
|
|
6106
|
+
payload: {
|
|
6107
|
+
error: err.message || String(err)
|
|
6108
|
+
}
|
|
6109
|
+
});
|
|
6110
|
+
throw err;
|
|
6111
|
+
}
|
|
6112
|
+
},
|
|
6113
|
+
// List views for the current site. `scope` is 'public' (anonymous-OK)
|
|
6114
|
+
// or 'private' (requires `user`).
|
|
6115
|
+
doListSavedViews: ({ scope: scope, user: user })=>async ({ dispatch: dispatch, store: store })=>{
|
|
6116
|
+
try {
|
|
6117
|
+
const site = store.selectConfiguration && store.selectConfiguration().id || '';
|
|
6118
|
+
let rows;
|
|
6119
|
+
if ($85a9f6732ceb79db$var$useMock()) rows = $85a9f6732ceb79db$var$mockList({
|
|
6120
|
+
site: site,
|
|
6121
|
+
scope: scope,
|
|
6122
|
+
uid: user && user.uid
|
|
6123
|
+
});
|
|
6124
|
+
else {
|
|
6125
|
+
const api = store.selectGrameneAPI();
|
|
6126
|
+
const headers = {
|
|
6127
|
+
Accept: 'application/json'
|
|
6128
|
+
};
|
|
6129
|
+
if (scope === 'private') {
|
|
6130
|
+
if (!user || !user.getIdToken) throw new Error('Not signed in');
|
|
6131
|
+
headers.Authorization = `Bearer ${await user.getIdToken()}`;
|
|
6132
|
+
}
|
|
6133
|
+
const url = `${api}/saved_views?site=${encodeURIComponent(site)}&isPublic=${scope === 'public'}`;
|
|
6134
|
+
const res = await fetch(url, {
|
|
6135
|
+
headers: headers
|
|
6136
|
+
});
|
|
6137
|
+
if (!res.ok) throw new Error(`List failed (${res.status})`);
|
|
6138
|
+
rows = await res.json();
|
|
6139
|
+
}
|
|
6140
|
+
dispatch({
|
|
6141
|
+
type: 'SAVED_VIEW_LIST_RECEIVED',
|
|
6142
|
+
payload: {
|
|
6143
|
+
kind: scope,
|
|
6144
|
+
rows: rows
|
|
6145
|
+
}
|
|
6146
|
+
});
|
|
6147
|
+
return rows;
|
|
6148
|
+
} catch (err) {
|
|
6149
|
+
dispatch({
|
|
6150
|
+
type: 'SAVED_VIEW_LIST_FAILED',
|
|
6151
|
+
payload: {
|
|
6152
|
+
error: err.message || String(err)
|
|
6153
|
+
}
|
|
6154
|
+
});
|
|
6155
|
+
throw err;
|
|
6156
|
+
}
|
|
6157
|
+
},
|
|
6158
|
+
// PATCH label / isPublic on a view I own. Mirrors updateList.
|
|
6159
|
+
doUpdateSavedView: ({ viewId: viewId, user: user, label: label, isPublic: isPublic })=>async ({ store: store })=>{
|
|
6160
|
+
const updates = {};
|
|
6161
|
+
if (typeof label === 'string') updates.label = label;
|
|
6162
|
+
if (typeof isPublic === 'boolean') updates.isPublic = isPublic;
|
|
6163
|
+
if (!Object.keys(updates).length) return;
|
|
6164
|
+
if ($85a9f6732ceb79db$var$useMock()) {
|
|
6165
|
+
$85a9f6732ceb79db$var$mockUpdate(viewId, updates);
|
|
6166
|
+
return {
|
|
6167
|
+
viewId: viewId,
|
|
6168
|
+
updates: updates
|
|
6169
|
+
};
|
|
6170
|
+
}
|
|
6171
|
+
const token = await user.getIdToken();
|
|
6172
|
+
const api = store.selectGrameneAPI();
|
|
6173
|
+
const res = await fetch(`${api}/saved_views?viewId=${encodeURIComponent(viewId)}`, {
|
|
6174
|
+
method: 'PATCH',
|
|
6175
|
+
headers: {
|
|
6176
|
+
'Content-Type': 'application/json',
|
|
6177
|
+
Authorization: `Bearer ${token}`
|
|
6178
|
+
},
|
|
6179
|
+
body: JSON.stringify(updates)
|
|
6180
|
+
});
|
|
6181
|
+
if (!res.ok) throw new Error(`Update failed (${res.status})`);
|
|
6182
|
+
return res.json();
|
|
6183
|
+
},
|
|
6184
|
+
// DELETE one of my saved views. Mirrors deleteList.
|
|
6185
|
+
doDeleteSavedView: ({ viewId: viewId, user: user })=>async ({ store: store })=>{
|
|
6186
|
+
if ($85a9f6732ceb79db$var$useMock()) {
|
|
6187
|
+
$85a9f6732ceb79db$var$mockDelete(viewId);
|
|
6188
|
+
return {
|
|
6189
|
+
viewId: viewId
|
|
6190
|
+
};
|
|
6191
|
+
}
|
|
6192
|
+
const token = await user.getIdToken();
|
|
6193
|
+
const api = store.selectGrameneAPI();
|
|
6194
|
+
const res = await fetch(`${api}/saved_views?viewId=${encodeURIComponent(viewId)}`, {
|
|
6195
|
+
method: 'DELETE',
|
|
6196
|
+
headers: {
|
|
6197
|
+
Authorization: `Bearer ${token}`
|
|
6198
|
+
}
|
|
6199
|
+
});
|
|
6200
|
+
if (!res.ok) throw new Error(`Delete failed (${res.status})`);
|
|
6201
|
+
return res.json();
|
|
6202
|
+
},
|
|
6203
|
+
doResetSavedViewState: ()=>({ dispatch: dispatch })=>dispatch({
|
|
6204
|
+
type: 'SAVED_VIEW_RESET'
|
|
6205
|
+
}),
|
|
6206
|
+
// Connect-friendly wrapper around bootViewFromUrl. Auth.js calls this on
|
|
6207
|
+
// each auth-state emission, passing the current Firebase user (or null);
|
|
6208
|
+
// bootView.js no-ops when ?view= isn't in the URL, so this is cheap to
|
|
6209
|
+
// call repeatedly.
|
|
6210
|
+
doBootSharedView: ({ user: user } = {})=>({ store: store })=>{
|
|
6211
|
+
return (0, $c69c61828e36a328$export$2e2bcd8739ae039)(store, {
|
|
6212
|
+
user: user
|
|
6213
|
+
});
|
|
6214
|
+
}
|
|
6215
|
+
};
|
|
6216
|
+
// ── helpers ─────────────────────────────────────────────────────────────
|
|
6217
|
+
// Test/dev: either set the runtime-only flag (`window.__SAVED_VIEWS_MOCK__ = true`,
|
|
6218
|
+
// cleared on reload), or persist via localStorage (`localStorage.setItem(
|
|
6219
|
+
// '__SAVED_VIEWS_MOCK__', 'true')`, survives reloads — needed for the
|
|
6220
|
+
// share-link round-trip which navigates).
|
|
6221
|
+
function $85a9f6732ceb79db$var$useMock() {
|
|
6222
|
+
if (typeof window === 'undefined') return false;
|
|
6223
|
+
if (window.__SAVED_VIEWS_MOCK__) return true;
|
|
6224
|
+
try {
|
|
6225
|
+
return typeof localStorage !== 'undefined' && localStorage.getItem('__SAVED_VIEWS_MOCK__') === 'true';
|
|
6226
|
+
} catch (_) {
|
|
6227
|
+
return false;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
function $85a9f6732ceb79db$var$buildShareUrl(hash) {
|
|
6231
|
+
if (typeof window === 'undefined') return `?view=${hash}`;
|
|
6232
|
+
const u = new URL(window.location.href);
|
|
6233
|
+
u.search = '';
|
|
6234
|
+
u.searchParams.set('view', hash);
|
|
6235
|
+
return u.toString();
|
|
6236
|
+
}
|
|
6237
|
+
// Content-addressable, ~72-bit short hash. The snapshot's `capturedAt` is
|
|
6238
|
+
// excluded from the hash input so two saves of the same view by the same
|
|
6239
|
+
// user produce the same hash (and the server's $setOnInsert preserves the
|
|
6240
|
+
// original createdAt).
|
|
6241
|
+
async function $85a9f6732ceb79db$var$computeContentHash(snapshot) {
|
|
6242
|
+
const forHash = {
|
|
6243
|
+
...snapshot,
|
|
6244
|
+
capturedAt: undefined
|
|
6245
|
+
};
|
|
6246
|
+
const text = $85a9f6732ceb79db$var$canonicalize(forHash);
|
|
6247
|
+
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
6248
|
+
const buf = new TextEncoder().encode(text);
|
|
6249
|
+
const digest = await crypto.subtle.digest('SHA-256', buf);
|
|
6250
|
+
const bytes = new Uint8Array(digest);
|
|
6251
|
+
let b64 = '';
|
|
6252
|
+
for(let i = 0; i < bytes.length; i++)b64 += String.fromCharCode(bytes[i]);
|
|
6253
|
+
return btoa(b64).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '').slice(0, 12);
|
|
6254
|
+
}
|
|
6255
|
+
// Last-resort fallback for environments without WebCrypto.
|
|
6256
|
+
let h = 0;
|
|
6257
|
+
for(let i = 0; i < text.length; i++)h = (h << 5) - h + text.charCodeAt(i) | 0;
|
|
6258
|
+
return ('h' + (h >>> 0).toString(36)).slice(0, 12);
|
|
6259
|
+
}
|
|
6260
|
+
// Stable stringify: sort object keys recursively so semantically-equivalent
|
|
6261
|
+
// snapshots hash identically.
|
|
6262
|
+
function $85a9f6732ceb79db$var$canonicalize(v) {
|
|
6263
|
+
if (v === null || typeof v !== 'object') return JSON.stringify(v);
|
|
6264
|
+
if (Array.isArray(v)) return '[' + v.map($85a9f6732ceb79db$var$canonicalize).join(',') + ']';
|
|
6265
|
+
const keys = Object.keys(v).filter((k)=>v[k] !== undefined).sort();
|
|
6266
|
+
return '{' + keys.map((k)=>JSON.stringify(k) + ':' + $85a9f6732ceb79db$var$canonicalize(v[k])).join(',') + '}';
|
|
6267
|
+
}
|
|
6268
|
+
// ── mock backend (localStorage) ─────────────────────────────────────────
|
|
6269
|
+
function $85a9f6732ceb79db$var$mockSave(row) {
|
|
6270
|
+
if (typeof localStorage === 'undefined') return;
|
|
6271
|
+
localStorage.setItem($85a9f6732ceb79db$var$STORAGE_PREFIX + row.hash, JSON.stringify({
|
|
6272
|
+
...row,
|
|
6273
|
+
_id: row.hash + ' ' + row.uid
|
|
6274
|
+
}));
|
|
6275
|
+
}
|
|
6276
|
+
function $85a9f6732ceb79db$var$mockFetch(hash) {
|
|
6277
|
+
if (typeof localStorage === 'undefined') return null;
|
|
6278
|
+
const raw = localStorage.getItem($85a9f6732ceb79db$var$STORAGE_PREFIX + hash);
|
|
6279
|
+
return raw ? JSON.parse(raw) : null;
|
|
6280
|
+
}
|
|
6281
|
+
function $85a9f6732ceb79db$var$mockList({ site: site, scope: scope, uid: uid }) {
|
|
6282
|
+
if (typeof localStorage === 'undefined') return [];
|
|
6283
|
+
const out = [];
|
|
6284
|
+
for(let i = 0; i < localStorage.length; i++){
|
|
6285
|
+
const k = localStorage.key(i);
|
|
6286
|
+
if (!k || !k.startsWith($85a9f6732ceb79db$var$STORAGE_PREFIX)) continue;
|
|
6287
|
+
const row = JSON.parse(localStorage.getItem(k));
|
|
6288
|
+
if (row.site !== site) continue;
|
|
6289
|
+
if (scope === 'public' && !row.isPublic) continue;
|
|
6290
|
+
if (scope === 'private' && (row.isPublic || row.uid !== uid)) continue;
|
|
6291
|
+
out.push(row);
|
|
6292
|
+
}
|
|
6293
|
+
return out.sort((a, b)=>(b.createdAt || '').localeCompare(a.createdAt || ''));
|
|
6294
|
+
}
|
|
6295
|
+
function $85a9f6732ceb79db$var$mockUpdate(viewId, updates) {
|
|
6296
|
+
if (typeof localStorage === 'undefined') return;
|
|
6297
|
+
for(let i = 0; i < localStorage.length; i++){
|
|
6298
|
+
const k = localStorage.key(i);
|
|
6299
|
+
if (!k || !k.startsWith($85a9f6732ceb79db$var$STORAGE_PREFIX)) continue;
|
|
6300
|
+
const row = JSON.parse(localStorage.getItem(k));
|
|
6301
|
+
if (row._id === viewId) {
|
|
6302
|
+
localStorage.setItem(k, JSON.stringify({
|
|
6303
|
+
...row,
|
|
6304
|
+
...updates
|
|
6305
|
+
}));
|
|
6306
|
+
return;
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
function $85a9f6732ceb79db$var$mockDelete(viewId) {
|
|
6311
|
+
if (typeof localStorage === 'undefined') return;
|
|
6312
|
+
for(let i = 0; i < localStorage.length; i++){
|
|
6313
|
+
const k = localStorage.key(i);
|
|
6314
|
+
if (!k || !k.startsWith($85a9f6732ceb79db$var$STORAGE_PREFIX)) continue;
|
|
6315
|
+
const row = JSON.parse(localStorage.getItem(k));
|
|
6316
|
+
if (row._id === viewId) {
|
|
6317
|
+
localStorage.removeItem(k);
|
|
6318
|
+
return;
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
var $85a9f6732ceb79db$export$2e2bcd8739ae039 = $85a9f6732ceb79db$var$savedViews;
|
|
6323
|
+
|
|
6324
|
+
|
|
5133
6325
|
var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
|
|
5134
6326
|
...(0, $9d9aeaf9299e61a1$export$2e2bcd8739ae039),
|
|
5135
6327
|
(0, $671312b287158a8a$export$2e2bcd8739ae039),
|
|
@@ -5140,7 +6332,10 @@ var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
|
|
|
5140
6332
|
(0, $1508f5a42be6e7b5$export$2e2bcd8739ae039),
|
|
5141
6333
|
(0, $c921a0d2b34aadb6$export$2e2bcd8739ae039),
|
|
5142
6334
|
(0, $4f15cd8a7d970b18$export$2e2bcd8739ae039),
|
|
5143
|
-
(0, $d365d8c287ab0c94$export$2e2bcd8739ae039)
|
|
6335
|
+
(0, $d365d8c287ab0c94$export$2e2bcd8739ae039),
|
|
6336
|
+
(0, $7f865ea0feda21af$export$2e2bcd8739ae039),
|
|
6337
|
+
(0, $472fe3745b238881$export$2e2bcd8739ae039),
|
|
6338
|
+
(0, $85a9f6732ceb79db$export$2e2bcd8739ae039)
|
|
5144
6339
|
];
|
|
5145
6340
|
|
|
5146
6341
|
|
|
@@ -5536,32 +6731,251 @@ const $541b8b0d8c5501d2$var$Suggestions = (props)=>{
|
|
|
5536
6731
|
]
|
|
5537
6732
|
}, idx);
|
|
5538
6733
|
}),
|
|
5539
|
-
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($541b8b0d8c5501d2$var$TryAnotherSite, {
|
|
5540
|
-
query: props.suggestionsQuery,
|
|
5541
|
-
config: props.configuration
|
|
6734
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($541b8b0d8c5501d2$var$TryAnotherSite, {
|
|
6735
|
+
query: props.suggestionsQuery,
|
|
6736
|
+
config: props.configuration
|
|
6737
|
+
})
|
|
6738
|
+
]
|
|
6739
|
+
});
|
|
6740
|
+
} else return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {});
|
|
6741
|
+
};
|
|
6742
|
+
var $541b8b0d8c5501d2$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneSuggestions', 'selectSuggestionsQuery', 'selectGrameneTaxonomy', 'selectConfiguration', 'doAcceptSuggestion', 'doAcceptGrameneSuggestion', $541b8b0d8c5501d2$var$Suggestions);
|
|
6743
|
+
|
|
6744
|
+
|
|
6745
|
+
|
|
6746
|
+
|
|
6747
|
+
|
|
6748
|
+
|
|
6749
|
+
|
|
6750
|
+
|
|
6751
|
+
|
|
6752
|
+
|
|
6753
|
+
|
|
6754
|
+
|
|
6755
|
+
|
|
6756
|
+
|
|
6757
|
+
|
|
6758
|
+
|
|
6759
|
+
|
|
6760
|
+
|
|
6761
|
+
// eFP Browser embed — was its own package (gramene-efp-browser@1.0.11),
|
|
6762
|
+
// absorbed here on 2026-05-29 to drop the unmaintained nwb/webpack-4 build
|
|
6763
|
+
// pipeline and remove a dependency that needed --openssl-legacy-provider.
|
|
6764
|
+
// All upstream URLs and species-specific id formatters preserved as-is.
|
|
6765
|
+
|
|
6766
|
+
|
|
6767
|
+
const $59a241f3912e631e$var$urls = {
|
|
6768
|
+
image: (genome, study, gene)=>`https://bar.utoronto.ca/api/efp_image/efp_${genome}/${study}/Absolute/${gene}`,
|
|
6769
|
+
app: (genome, study, gene)=>`https://bar.utoronto.ca/efp_${genome}/cgi-bin/efpWeb.cgi?dataSource=${study}&mode=Absolute&primaryGene=${gene}`,
|
|
6770
|
+
studies: (genome)=>`https://bar.utoronto.ca/api/efp_image/get_efp_data_source/${genome}`,
|
|
6771
|
+
logo: 'https://bar.utoronto.ca/bbc_logo_small.gif',
|
|
6772
|
+
spinner: 'https://www.sorghumbase.org/static/images/dna_spinner.svg'
|
|
6773
|
+
};
|
|
6774
|
+
const $59a241f3912e631e$var$zmv4_re = /Zm00001d/;
|
|
6775
|
+
const $59a241f3912e631e$var$browsers = {
|
|
6776
|
+
sorghum_bicolor: {
|
|
6777
|
+
formatGene: (gene)=>gene._id.replace('SORBI_3', 'Sobic.'),
|
|
6778
|
+
genome: 'sorghum'
|
|
6779
|
+
},
|
|
6780
|
+
vitis_vinifera: {
|
|
6781
|
+
formatGene: (gene)=>gene._id,
|
|
6782
|
+
genome: 'grape'
|
|
6783
|
+
},
|
|
6784
|
+
arabidopsis_thaliana: {
|
|
6785
|
+
formatGene: (gene)=>gene._id,
|
|
6786
|
+
fixStudies: (studies)=>{
|
|
6787
|
+
studies = studies.filter((s)=>s.value !== 'Klepikova_Atlas');
|
|
6788
|
+
studies.unshift({
|
|
6789
|
+
value: 'Klepikova_Atlas',
|
|
6790
|
+
label: 'Klepikova Atlas'
|
|
6791
|
+
});
|
|
6792
|
+
return studies;
|
|
6793
|
+
},
|
|
6794
|
+
genome: 'arabidopsis'
|
|
6795
|
+
},
|
|
6796
|
+
zea_mays: {
|
|
6797
|
+
formatGene: (gene)=>{
|
|
6798
|
+
let id = gene._id;
|
|
6799
|
+
if (gene.synonyms) gene.synonyms.forEach((syn)=>{
|
|
6800
|
+
if ($59a241f3912e631e$var$zmv4_re.test(syn)) id = syn;
|
|
6801
|
+
});
|
|
6802
|
+
return id;
|
|
6803
|
+
},
|
|
6804
|
+
fixStudies: (studies)=>{
|
|
6805
|
+
studies = studies.filter((s)=>s.value !== 'Hoopes_et_al_Atlas' && s.value !== 'Hoopes_et_al_Stress');
|
|
6806
|
+
studies.unshift({
|
|
6807
|
+
value: 'Hoopes_et_al_Stress',
|
|
6808
|
+
label: 'Hoopes et. al., Stress'
|
|
6809
|
+
});
|
|
6810
|
+
studies.unshift({
|
|
6811
|
+
value: 'Hoopes_et_al_Atlas',
|
|
6812
|
+
label: 'Hoopes et. al., Atlas'
|
|
6813
|
+
});
|
|
6814
|
+
return studies;
|
|
6815
|
+
},
|
|
6816
|
+
genome: 'maize'
|
|
6817
|
+
},
|
|
6818
|
+
glycine_max: {
|
|
6819
|
+
formatGene: (gene)=>gene._id.replace('GLYMA_', 'Glyma.'),
|
|
6820
|
+
fixStudies: (studies)=>studies.filter((s)=>s.value !== 'soybean_senescence'),
|
|
6821
|
+
genome: 'soybean'
|
|
6822
|
+
},
|
|
6823
|
+
oryza_sativa: {
|
|
6824
|
+
genome: 'rice',
|
|
6825
|
+
formatGene: (gene)=>gene.MSU_id,
|
|
6826
|
+
fixStudies: (studies)=>{
|
|
6827
|
+
studies = studies.filter((s)=>s.value !== 'rice_rma' && s.value !== 'rice_mas');
|
|
6828
|
+
studies.unshift({
|
|
6829
|
+
value: 'rice_rma',
|
|
6830
|
+
label: 'rice rma'
|
|
6831
|
+
});
|
|
6832
|
+
studies.unshift({
|
|
6833
|
+
value: 'rice_mas',
|
|
6834
|
+
label: 'rice mas'
|
|
6835
|
+
});
|
|
6836
|
+
return studies;
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
};
|
|
6840
|
+
// Subsites that refer to maize b73 as `zea_maysb73`.
|
|
6841
|
+
$59a241f3912e631e$var$browsers.zea_maysb73 = $59a241f3912e631e$var$browsers.zea_mays;
|
|
6842
|
+
const $59a241f3912e631e$var$ImageLoader = ({ url: url })=>{
|
|
6843
|
+
const [loading, setLoading] = (0, $gXNCa$react.useState)(true);
|
|
6844
|
+
const [error, setError] = (0, $gXNCa$react.useState)(false);
|
|
6845
|
+
(0, $gXNCa$react.useEffect)(()=>{
|
|
6846
|
+
setLoading(true);
|
|
6847
|
+
setError(false);
|
|
6848
|
+
const image = new Image();
|
|
6849
|
+
image.src = url;
|
|
6850
|
+
image.onload = ()=>setLoading(false);
|
|
6851
|
+
image.onerror = ()=>{
|
|
6852
|
+
setLoading(false);
|
|
6853
|
+
setError(true);
|
|
6854
|
+
};
|
|
6855
|
+
}, [
|
|
6856
|
+
url
|
|
6857
|
+
]);
|
|
6858
|
+
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
6859
|
+
style: {
|
|
6860
|
+
padding: 20
|
|
6861
|
+
},
|
|
6862
|
+
children: [
|
|
6863
|
+
loading && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
|
|
6864
|
+
src: $59a241f3912e631e$var$urls.spinner,
|
|
6865
|
+
alt: "Loading\u2026"
|
|
6866
|
+
}),
|
|
6867
|
+
!loading && !error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
|
|
6868
|
+
style: {
|
|
6869
|
+
maxWidth: '100%'
|
|
6870
|
+
},
|
|
6871
|
+
src: url,
|
|
6872
|
+
alt: "eFP browser output"
|
|
6873
|
+
}),
|
|
6874
|
+
error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("p", {
|
|
6875
|
+
children: "Error: Failed to load image"
|
|
6876
|
+
})
|
|
6877
|
+
]
|
|
6878
|
+
});
|
|
6879
|
+
};
|
|
6880
|
+
class $59a241f3912e631e$export$2e2bcd8739ae039 extends (0, $gXNCa$react.Component) {
|
|
6881
|
+
constructor(props){
|
|
6882
|
+
super(props);
|
|
6883
|
+
this.state = {
|
|
6884
|
+
currentStudy: null
|
|
6885
|
+
};
|
|
6886
|
+
}
|
|
6887
|
+
getStudies(browser) {
|
|
6888
|
+
fetch($59a241f3912e631e$var$urls.studies(browser.genome)).then((res)=>res.json()).then((res)=>{
|
|
6889
|
+
if (res.wasSuccessful) {
|
|
6890
|
+
let studies = res.data.sort().map((v)=>({
|
|
6891
|
+
value: v,
|
|
6892
|
+
label: v.replace(/_/g, ' ')
|
|
6893
|
+
}));
|
|
6894
|
+
if (browser.fixStudies) studies = browser.fixStudies(studies);
|
|
6895
|
+
this.setState({
|
|
6896
|
+
studies: studies
|
|
6897
|
+
});
|
|
6898
|
+
}
|
|
6899
|
+
}).catch(console.error);
|
|
6900
|
+
}
|
|
6901
|
+
render() {
|
|
6902
|
+
const gene = this.props.gene;
|
|
6903
|
+
const browser = $59a241f3912e631e$var$browsers[gene.system_name];
|
|
6904
|
+
if (!browser) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
6905
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("h2", {
|
|
6906
|
+
children: [
|
|
6907
|
+
"Can't find eFP browser for ",
|
|
6908
|
+
gene.system_name
|
|
6909
|
+
]
|
|
6910
|
+
})
|
|
6911
|
+
});
|
|
6912
|
+
if (!this.state.studies) {
|
|
6913
|
+
this.getStudies(browser);
|
|
6914
|
+
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
|
|
6915
|
+
src: $59a241f3912e631e$var$urls.spinner,
|
|
6916
|
+
alt: "Loading\u2026"
|
|
6917
|
+
});
|
|
6918
|
+
}
|
|
6919
|
+
const efp_gene = browser.formatGene(gene);
|
|
6920
|
+
// `study`/`onStudyChange` let a host drive the selection (so a saved view
|
|
6921
|
+
// can restore it); fall back to local state when uncontrolled.
|
|
6922
|
+
const study = this.props.study || this.state.currentStudy || this.state.studies[0].value;
|
|
6923
|
+
const onSelect = (e)=>{
|
|
6924
|
+
if (this.props.onStudyChange) this.props.onStudyChange(e.target.value);
|
|
6925
|
+
else this.setState({
|
|
6926
|
+
currentStudy: e.target.value
|
|
6927
|
+
});
|
|
6928
|
+
};
|
|
6929
|
+
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
6930
|
+
style: {
|
|
6931
|
+
paddingTop: 10
|
|
6932
|
+
},
|
|
6933
|
+
children: [
|
|
6934
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("label", {
|
|
6935
|
+
style: {
|
|
6936
|
+
paddingLeft: 20,
|
|
6937
|
+
paddingRight: 10
|
|
6938
|
+
},
|
|
6939
|
+
children: "Select a study:"
|
|
6940
|
+
}),
|
|
6941
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("select", {
|
|
6942
|
+
value: study,
|
|
6943
|
+
onChange: onSelect,
|
|
6944
|
+
children: this.state.studies.map((s, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("option", {
|
|
6945
|
+
value: s.value,
|
|
6946
|
+
children: s.label
|
|
6947
|
+
}, idx))
|
|
6948
|
+
}),
|
|
6949
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("br", {}),
|
|
6950
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($59a241f3912e631e$var$ImageLoader, {
|
|
6951
|
+
url: $59a241f3912e631e$var$urls.image(browser.genome, study, efp_gene)
|
|
6952
|
+
}),
|
|
6953
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("a", {
|
|
6954
|
+
style: {
|
|
6955
|
+
paddingLeft: 100
|
|
6956
|
+
},
|
|
6957
|
+
href: $59a241f3912e631e$var$urls.app(browser.genome, study, efp_gene),
|
|
6958
|
+
target: "_blank",
|
|
6959
|
+
rel: "noreferrer",
|
|
6960
|
+
children: [
|
|
6961
|
+
"Powered by ",
|
|
6962
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("img", {
|
|
6963
|
+
src: $59a241f3912e631e$var$urls.logo,
|
|
6964
|
+
style: {
|
|
6965
|
+
maxWidth: 40
|
|
6966
|
+
},
|
|
6967
|
+
alt: "BBC"
|
|
6968
|
+
}),
|
|
6969
|
+
" Webservices"
|
|
6970
|
+
]
|
|
5542
6971
|
})
|
|
5543
6972
|
]
|
|
5544
6973
|
});
|
|
5545
|
-
}
|
|
5546
|
-
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6976
|
+
function $59a241f3912e631e$export$1bc0f3596ef2efe9(gene) {
|
|
6977
|
+
return $59a241f3912e631e$var$browsers.hasOwnProperty(gene.system_name);
|
|
6978
|
+
}
|
|
5565
6979
|
|
|
5566
6980
|
|
|
5567
6981
|
function $9e29a4f60318db7a$var$DynamicIframe(props) {
|
|
@@ -5587,15 +7001,43 @@ function $9e29a4f60318db7a$var$DynamicIframe(props) {
|
|
|
5587
7001
|
});
|
|
5588
7002
|
}
|
|
5589
7003
|
const $9e29a4f60318db7a$var$Detail = (props)=>{
|
|
5590
|
-
const
|
|
5591
|
-
const
|
|
7004
|
+
const geneId = props.searchResult.id;
|
|
7005
|
+
const gene = props.geneDocs[geneId];
|
|
7006
|
+
// User-selected view state (active sub-tab, chosen GXA experiment, chosen eFP
|
|
7007
|
+
// study) lives in the uiViewState bundle keyed by geneId, so the shareable-
|
|
7008
|
+
// views snapshot can round-trip it. Fetched data + the dev-only Local API
|
|
7009
|
+
// toggle stay in local state. Defaults match the old local-state initials.
|
|
7010
|
+
const expr = props.uiViewState && props.uiViewState.byGene[geneId] && props.uiViewState.byGene[geneId].expression || {};
|
|
7011
|
+
const activeTab = expr.activeTab || 'gene';
|
|
7012
|
+
const atlasExperiment = expr.atlasExperiment || null;
|
|
7013
|
+
const setActiveTab = (k)=>props.doSetExpressionState({
|
|
7014
|
+
geneId: geneId,
|
|
7015
|
+
patch: {
|
|
7016
|
+
activeTab: k
|
|
7017
|
+
}
|
|
7018
|
+
});
|
|
7019
|
+
const setAtlasExperiment = (v)=>props.doSetExpressionState({
|
|
7020
|
+
geneId: geneId,
|
|
7021
|
+
patch: {
|
|
7022
|
+
atlasExperiment: v
|
|
7023
|
+
}
|
|
7024
|
+
});
|
|
5592
7025
|
const [atlasExperimentList, setAtlasExperimentList] = (0, $gXNCa$react.useState)([]);
|
|
5593
7026
|
const [atlasFacets, setAtlasFacets] = (0, $gXNCa$react.useState)(null);
|
|
5594
7027
|
const [isLocal, setIsLocal] = (0, $gXNCa$react.useState)(false);
|
|
5595
|
-
const [activeTab, setActiveTab] = (0, $gXNCa$react.useState)('gene');
|
|
5596
7028
|
const handleLocalAPIChange = (event)=>{
|
|
5597
7029
|
setIsLocal(event.target.checked);
|
|
5598
7030
|
};
|
|
7031
|
+
// The expressionStudies resource is otherwise fetched only when a top-level
|
|
7032
|
+
// expression view (exprViz/expression/export) is on — but this per-gene
|
|
7033
|
+
// Expression detail also needs it (the Paralogs sub-tab's experiment list and
|
|
7034
|
+
// the atlasExperiment selection both derive from it). Self-fetch on mount so
|
|
7035
|
+
// opening the detail populates the studies even when no such view is enabled.
|
|
7036
|
+
(0, $gXNCa$react.useEffect)(()=>{
|
|
7037
|
+
if (!props.expressionStudies && props.doFetchExpressionStudies) props.doFetchExpressionStudies();
|
|
7038
|
+
}, [
|
|
7039
|
+
props.expressionStudies
|
|
7040
|
+
]);
|
|
5599
7041
|
(0, $gXNCa$react.useEffect)(()=>{
|
|
5600
7042
|
if (!props.expressionStudies) return;
|
|
5601
7043
|
const tid = Math.floor(gene.taxon_id / 1000);
|
|
@@ -5618,10 +7060,15 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
|
|
|
5618
7060
|
});
|
|
5619
7061
|
setAtlasExperimentList(eList);
|
|
5620
7062
|
setAtlasFacets(facets);
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
7063
|
+
// Only pick a default experiment when the user (or a restored snapshot)
|
|
7064
|
+
// hasn't already chosen one — otherwise we'd clobber a saved selection
|
|
7065
|
+
// the moment the studies list loads.
|
|
7066
|
+
if (!atlasExperiment) {
|
|
7067
|
+
let refExp = eList.filter((e)=>e.isRef);
|
|
7068
|
+
if (refExp.length === 1) setAtlasExperiment(refExp[0]._id);
|
|
7069
|
+
else // no reference experiment - choose first
|
|
7070
|
+
setAtlasExperiment(eList[0]._id);
|
|
7071
|
+
}
|
|
5625
7072
|
}
|
|
5626
7073
|
}, [
|
|
5627
7074
|
props.expressionStudies
|
|
@@ -5653,6 +7100,7 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
|
|
|
5653
7100
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
|
|
5654
7101
|
"aria-label": "experiment selector",
|
|
5655
7102
|
placeholder: "Select experiment",
|
|
7103
|
+
value: atlasExperiment || '',
|
|
5656
7104
|
onChange: (e)=>setAtlasExperiment(e.target.value),
|
|
5657
7105
|
children: atlasExperimentList.map((e, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
|
|
5658
7106
|
value: e._id,
|
|
@@ -5676,18 +7124,25 @@ const $9e29a4f60318db7a$var$Detail = (props)=>{
|
|
|
5676
7124
|
url: gene_url
|
|
5677
7125
|
})
|
|
5678
7126
|
}, "gxa"),
|
|
5679
|
-
(0, $
|
|
7127
|
+
(0, $59a241f3912e631e$export$1bc0f3596ef2efe9)(gene) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
|
|
5680
7128
|
tabClassName: "eFP",
|
|
5681
7129
|
eventKey: "eFP",
|
|
5682
7130
|
title: "eFP Browser",
|
|
5683
|
-
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0,
|
|
5684
|
-
gene: gene
|
|
7131
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $59a241f3912e631e$export$2e2bcd8739ae039), {
|
|
7132
|
+
gene: gene,
|
|
7133
|
+
study: expr.barStudy,
|
|
7134
|
+
onStudyChange: (v)=>props.doSetExpressionState({
|
|
7135
|
+
geneId: geneId,
|
|
7136
|
+
patch: {
|
|
7137
|
+
barStudy: v
|
|
7138
|
+
}
|
|
7139
|
+
})
|
|
5685
7140
|
})
|
|
5686
7141
|
}, "bar")
|
|
5687
7142
|
]
|
|
5688
7143
|
});
|
|
5689
7144
|
};
|
|
5690
|
-
var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneParalogs', 'selectExpressionStudies', 'doRequestParalogs', //'doRequestParalogExpression',
|
|
7145
|
+
var $9e29a4f60318db7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneParalogs', 'selectExpressionStudies', 'selectUiViewState', 'doRequestParalogs', 'doFetchExpressionStudies', 'doSetExpressionState', //'doRequestParalogExpression',
|
|
5691
7146
|
$9e29a4f60318db7a$var$Detail);
|
|
5692
7147
|
|
|
5693
7148
|
|
|
@@ -5706,6 +7161,7 @@ $9e29a4f60318db7a$var$Detail);
|
|
|
5706
7161
|
|
|
5707
7162
|
|
|
5708
7163
|
|
|
7164
|
+
|
|
5709
7165
|
const $5c2c79352d3d7b81$export$ec6b54b688be1708 = ({ fullscreen: fullscreen, onExitFullscreen: onExitFullscreen, title: title, className: className, children: children })=>{
|
|
5710
7166
|
const [stableNode] = (0, $gXNCa$react.useState)(()=>typeof document !== 'undefined' ? document.createElement('div') : null);
|
|
5711
7167
|
const inlineRef = (0, $gXNCa$react.useRef)(null);
|
|
@@ -5923,20 +7379,140 @@ const $64fad37f770d2bfe$var$TBROWSE_ZONES = [
|
|
|
5923
7379
|
class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
|
|
5924
7380
|
constructor(props){
|
|
5925
7381
|
super(props);
|
|
7382
|
+
// Async data caches stay in local state (per mount, per tree). The
|
|
7383
|
+
// user-controlled view state (viewer toggle, resize height, tbrowse
|
|
7384
|
+
// ViewState) is lifted into the uiViewState bundle, keyed by geneId.
|
|
7385
|
+
//
|
|
7386
|
+
// `*Status` fields track per-zone load lifecycle so the TBrowse
|
|
7387
|
+
// toolbar can render loading/error affordances via its zoneStatus
|
|
7388
|
+
// prop. They start `undefined` (no signal); flipped to 'loading'
|
|
7389
|
+
// when the fetch kicks off and 'ready' / 'error' on settle.
|
|
5926
7390
|
this.state = {
|
|
5927
|
-
viewer: 'treevis',
|
|
5928
7391
|
neighborhood: null,
|
|
5929
7392
|
neighborhoodTreeId: null,
|
|
7393
|
+
neighborhoodStatus: undefined,
|
|
5930
7394
|
geneStructures: null,
|
|
5931
7395
|
geneStructuresTreeId: null,
|
|
5932
|
-
|
|
7396
|
+
geneStructuresStatus: undefined
|
|
5933
7397
|
};
|
|
5934
7398
|
if (!props.geneDocs.hasOwnProperty(props.searchResult.id)) props.requestGene(props.searchResult.id);
|
|
5935
|
-
this.taxonomy = (0, ($parcel$interopDefault($gXNCa$
|
|
7399
|
+
this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
|
|
7400
|
+
}
|
|
7401
|
+
// ----- uiViewState accessors (with sensible defaults) -----
|
|
7402
|
+
getGeneId() {
|
|
7403
|
+
return this.props.searchResult.id;
|
|
7404
|
+
}
|
|
7405
|
+
getHomologySlice() {
|
|
7406
|
+
const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.getGeneId()];
|
|
7407
|
+
return slice && slice.homology || {};
|
|
7408
|
+
}
|
|
7409
|
+
getViewer() {
|
|
7410
|
+
return this.getHomologySlice().viewer || 'treevis';
|
|
7411
|
+
}
|
|
7412
|
+
getHeight() {
|
|
7413
|
+
const h = this.getHomologySlice().height;
|
|
7414
|
+
return typeof h === 'number' ? h : 600;
|
|
7415
|
+
}
|
|
7416
|
+
setViewer(viewer) {
|
|
7417
|
+
this.props.doSetHomologyViewer({
|
|
7418
|
+
geneId: this.getGeneId(),
|
|
7419
|
+
viewer: viewer
|
|
7420
|
+
});
|
|
7421
|
+
}
|
|
7422
|
+
setHeight(height) {
|
|
7423
|
+
this.props.doSetHomologyHeight({
|
|
7424
|
+
geneId: this.getGeneId(),
|
|
7425
|
+
height: height
|
|
7426
|
+
});
|
|
7427
|
+
}
|
|
7428
|
+
setTbrowseViewState(viewState) {
|
|
7429
|
+
this.props.doSetHomologyTbrowseViewState({
|
|
7430
|
+
geneId: this.getGeneId(),
|
|
7431
|
+
tbrowse: viewState
|
|
7432
|
+
});
|
|
7433
|
+
}
|
|
7434
|
+
// Seed the bundle with a pivot-computed initial tbrowse view state once the
|
|
7435
|
+
// tree data is available and the user is actually looking at the tbrowse
|
|
7436
|
+
// viewer. Called from lifecycle (not render) to avoid dispatching mid-render.
|
|
7437
|
+
maybeSeedTbrowseViewState() {
|
|
7438
|
+
if (this.getViewer() !== 'tbrowse') return;
|
|
7439
|
+
if (this.getHomologySlice().tbrowse) return;
|
|
7440
|
+
const id = this.getGeneId();
|
|
7441
|
+
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
7442
|
+
const gene = this.props.geneDocs[id];
|
|
7443
|
+
if (!gene.homology) return;
|
|
7444
|
+
const treeId = gene.homology.gene_tree.id;
|
|
7445
|
+
const raw = this.props.grameneTrees[treeId];
|
|
7446
|
+
if (!raw || !raw.taxon_id) return;
|
|
7447
|
+
const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
|
|
7448
|
+
raw
|
|
7449
|
+
]);
|
|
7450
|
+
const pivot = (0, $gXNCa$tbrowse.computePivotState)(adapted.tree, gene._id);
|
|
7451
|
+
this.setTbrowseViewState({
|
|
7452
|
+
selectedNodeId: null,
|
|
7453
|
+
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
7454
|
+
prunedNodeIds: [],
|
|
7455
|
+
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
7456
|
+
compressedNodeIds: [],
|
|
7457
|
+
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
7458
|
+
// Use each zone's intended fr-share + initial visibility from its
|
|
7459
|
+
// definition (tree:30 / labels:20 / msa:50 / neighborhood:30 /
|
|
7460
|
+
// genome:32) instead of uniform 25 across the board. Gives the MSA
|
|
7461
|
+
// a wider initial pane and the tree a narrower one.
|
|
7462
|
+
//
|
|
7463
|
+
// `defaultVisible ?? true` matches tbrowse's own buildInitialViewState
|
|
7464
|
+
// logic — zones default to visible unless their definition opts out
|
|
7465
|
+
// (e.g. neighborhood and genome opt out so they only appear once
|
|
7466
|
+
// their async data lands; tbrowse's Layout auto-flips them on then).
|
|
7467
|
+
zones: $64fad37f770d2bfe$var$TBROWSE_ZONES.map((z)=>({
|
|
7468
|
+
id: z.id,
|
|
7469
|
+
width: z.defaultWidth,
|
|
7470
|
+
visible: z.defaultVisible ?? true
|
|
7471
|
+
})),
|
|
7472
|
+
zoneStates: {},
|
|
7473
|
+
search: null
|
|
7474
|
+
});
|
|
7475
|
+
}
|
|
7476
|
+
componentDidMount() {
|
|
7477
|
+
this.maybeSeedTbrowseViewState();
|
|
7478
|
+
this.maybeFetchTbrowseData();
|
|
7479
|
+
}
|
|
7480
|
+
componentDidUpdate() {
|
|
7481
|
+
this.maybeSeedTbrowseViewState();
|
|
7482
|
+
this.maybeFetchTbrowseData();
|
|
7483
|
+
}
|
|
7484
|
+
// Kick off the neighborhood + gene-structure fetches from lifecycle,
|
|
7485
|
+
// not render. Each fetch internally dedupes on `_*FetchedFor === treeId`
|
|
7486
|
+
// so calling on every commit is cheap. Gated on the user actually being
|
|
7487
|
+
// on the tbrowse viewer + the tree data being loaded — otherwise we'd
|
|
7488
|
+
// pay network cost for genes the user never looks at.
|
|
7489
|
+
maybeFetchTbrowseData() {
|
|
7490
|
+
if (this.getViewer() !== 'tbrowse') return;
|
|
7491
|
+
const id = this.getGeneId();
|
|
7492
|
+
if (!this.props.geneDocs.hasOwnProperty(id)) return;
|
|
7493
|
+
const gene = this.props.geneDocs[id];
|
|
7494
|
+
if (!gene.homology) return;
|
|
7495
|
+
const treeId = gene.homology.gene_tree.id;
|
|
7496
|
+
const raw = this.props.grameneTrees[treeId];
|
|
7497
|
+
if (!raw || !raw.taxon_id) return;
|
|
7498
|
+
// Lazily compute the adapted tbrowse data the same way renderTBrowse
|
|
7499
|
+
// does, so fetchGeneStructures has the leaf ids without rerunning
|
|
7500
|
+
// fromGrameneGenetree on every commit.
|
|
7501
|
+
if (this._tbrowseTreeId !== treeId) {
|
|
7502
|
+
this._tbrowseTreeId = treeId;
|
|
7503
|
+
this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
|
|
7504
|
+
raw
|
|
7505
|
+
]);
|
|
7506
|
+
}
|
|
7507
|
+
this.fetchNeighborhood(treeId);
|
|
7508
|
+
this.fetchGeneStructures(treeId, this._tbrowseData.tree);
|
|
5936
7509
|
}
|
|
5937
7510
|
fetchNeighborhood(treeId) {
|
|
5938
7511
|
if (this._neighborhoodFetchedFor === treeId) return;
|
|
5939
7512
|
this._neighborhoodFetchedFor = treeId;
|
|
7513
|
+
this.setState({
|
|
7514
|
+
neighborhoodStatus: 'loading'
|
|
7515
|
+
});
|
|
5940
7516
|
const api = this.props.grameneAPI;
|
|
5941
7517
|
const url = new URL(`${api}/search`);
|
|
5942
7518
|
url.searchParams.set('fl', 'id,name,gene_tree,gene_idx,region,start,end,strand,biotype,system_name,description');
|
|
@@ -5954,10 +7530,16 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
5954
7530
|
if (this._neighborhoodFetchedFor !== treeId) return;
|
|
5955
7531
|
this.setState({
|
|
5956
7532
|
neighborhood: (0, $gXNCa$tbrowse.fromGrameneNeighborhood)(json),
|
|
5957
|
-
neighborhoodTreeId: treeId
|
|
7533
|
+
neighborhoodTreeId: treeId,
|
|
7534
|
+
neighborhoodStatus: 'ready'
|
|
5958
7535
|
});
|
|
5959
7536
|
}).catch((err)=>{
|
|
5960
7537
|
console.warn('tbrowse neighborhood fetch failed:', err);
|
|
7538
|
+
// Surface the failure via the zoneStatus prop. Reset the
|
|
7539
|
+
// dedupe key so a re-mount or tree-change can retry.
|
|
7540
|
+
if (this._neighborhoodFetchedFor === treeId) this.setState({
|
|
7541
|
+
neighborhoodStatus: 'error'
|
|
7542
|
+
});
|
|
5961
7543
|
this._neighborhoodFetchedFor = null;
|
|
5962
7544
|
});
|
|
5963
7545
|
}
|
|
@@ -5966,6 +7548,9 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
5966
7548
|
this._geneStructuresFetchedFor = treeId;
|
|
5967
7549
|
const ids = Object.values(tree.nodes).filter((n)=>n.isLeaf && n.geneId).map((n)=>n.geneId);
|
|
5968
7550
|
if (ids.length === 0) return;
|
|
7551
|
+
this.setState({
|
|
7552
|
+
geneStructuresStatus: 'loading'
|
|
7553
|
+
});
|
|
5969
7554
|
const api = this.props.grameneAPI;
|
|
5970
7555
|
const BATCH_SIZE = 50;
|
|
5971
7556
|
const batches = [];
|
|
@@ -5988,22 +7573,24 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
5988
7573
|
const combined = [].concat(...results.map((r)=>Array.isArray(r) ? r : []));
|
|
5989
7574
|
this.setState({
|
|
5990
7575
|
geneStructures: (0, $gXNCa$tbrowse.fromGrameneGeneStructures)(combined),
|
|
5991
|
-
geneStructuresTreeId: treeId
|
|
7576
|
+
geneStructuresTreeId: treeId,
|
|
7577
|
+
geneStructuresStatus: 'ready'
|
|
5992
7578
|
});
|
|
5993
7579
|
}).catch((err)=>{
|
|
5994
7580
|
console.warn('tbrowse gene-structures fetch failed:', err);
|
|
7581
|
+
if (this._geneStructuresFetchedFor === treeId) this.setState({
|
|
7582
|
+
geneStructuresStatus: 'error'
|
|
7583
|
+
});
|
|
5995
7584
|
this._geneStructuresFetchedFor = null;
|
|
5996
7585
|
});
|
|
5997
7586
|
}
|
|
5998
7587
|
startResize(e) {
|
|
5999
7588
|
e.preventDefault();
|
|
6000
7589
|
const startY = e.clientY;
|
|
6001
|
-
const startHeight = this.
|
|
7590
|
+
const startHeight = this.getHeight();
|
|
6002
7591
|
const onMouseMove = (moveEvent)=>{
|
|
6003
7592
|
const newHeight = Math.max(200, startHeight + (moveEvent.clientY - startY));
|
|
6004
|
-
this.
|
|
6005
|
-
height: newHeight
|
|
6006
|
-
});
|
|
7593
|
+
this.setHeight(newHeight);
|
|
6007
7594
|
};
|
|
6008
7595
|
const onMouseUp = ()=>{
|
|
6009
7596
|
window.removeEventListener('mousemove', onMouseMove);
|
|
@@ -6026,7 +7613,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6026
7613
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
6027
7614
|
className: "gene-genetree",
|
|
6028
7615
|
style: {
|
|
6029
|
-
height: this.
|
|
7616
|
+
height: this.getHeight(),
|
|
6030
7617
|
width: '100%'
|
|
6031
7618
|
},
|
|
6032
7619
|
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, ($parcel$interopDefault($gXNCa$gramenegenetreevis))), {
|
|
@@ -6048,41 +7635,36 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6048
7635
|
}
|
|
6049
7636
|
renderTBrowse() {
|
|
6050
7637
|
const treeId = this.gene.homology.gene_tree.id;
|
|
7638
|
+
// Adapted tbrowse data is computed lazily in maybeFetchTbrowseData
|
|
7639
|
+
// (called from lifecycle). If the user just flipped to tbrowse and
|
|
7640
|
+
// the lifecycle hasn't fired yet, compute it once here without
|
|
7641
|
+
// calling any fetches — those will fire from componentDidUpdate.
|
|
6051
7642
|
if (this._tbrowseTreeId !== treeId) {
|
|
6052
|
-
const raw = this.props.grameneTrees[treeId];
|
|
6053
|
-
const adapted = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
|
|
6054
|
-
raw
|
|
6055
|
-
]);
|
|
6056
7643
|
this._tbrowseTreeId = treeId;
|
|
6057
|
-
this._tbrowseData =
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
this._tbrowseInitialViewState = {
|
|
6061
|
-
selectedNodeId: null,
|
|
6062
|
-
collapsedNodeIds: pivot ? pivot.collapsedNodeIds : [],
|
|
6063
|
-
prunedNodeIds: [],
|
|
6064
|
-
swappedNodeIds: pivot ? pivot.swappedNodeIds : [],
|
|
6065
|
-
compressedNodeIds: [],
|
|
6066
|
-
nodeOfInterestId: pivot ? pivot.targetId : null,
|
|
6067
|
-
zones: zoneIds.map((id)=>({
|
|
6068
|
-
id: id,
|
|
6069
|
-
width: 25,
|
|
6070
|
-
visible: true
|
|
6071
|
-
})),
|
|
6072
|
-
zoneStates: {},
|
|
6073
|
-
search: null
|
|
6074
|
-
};
|
|
7644
|
+
this._tbrowseData = (0, $gXNCa$tbrowse.fromGrameneGenetree)([
|
|
7645
|
+
this.props.grameneTrees[treeId]
|
|
7646
|
+
]);
|
|
6075
7647
|
}
|
|
6076
|
-
this.fetchNeighborhood(treeId);
|
|
6077
|
-
this.fetchGeneStructures(treeId, this._tbrowseData.tree);
|
|
6078
7648
|
const neighborhood = this.state.neighborhoodTreeId === treeId ? this.state.neighborhood : undefined;
|
|
6079
7649
|
const geneStructures = this.state.geneStructuresTreeId === treeId ? this.state.geneStructures : undefined;
|
|
7650
|
+
// Per-zone status for the TBrowse toolbar — pulses while a fetch
|
|
7651
|
+
// is in flight, turns red on failure. Tracked per-tree so a
|
|
7652
|
+
// tree-change resets any stale 'ready'/'error' from the prior gene.
|
|
7653
|
+
const zoneStatus = {
|
|
7654
|
+
neighborhood: this.state.neighborhoodTreeId === treeId ? this.state.neighborhoodStatus : this.state.neighborhoodStatus === 'loading' ? 'loading' : undefined,
|
|
7655
|
+
genome: this.state.geneStructuresTreeId === treeId ? this.state.geneStructuresStatus : this.state.geneStructuresStatus === 'loading' ? 'loading' : undefined
|
|
7656
|
+
};
|
|
7657
|
+
// Bundle-driven (controlled) view state. If we're rendering tbrowse before
|
|
7658
|
+
// componentDidMount/Update has seeded the bundle slice, skip this turn and
|
|
7659
|
+
// let the re-render with the seeded state do the work.
|
|
7660
|
+
const tbrowseVS = this.getHomologySlice().tbrowse;
|
|
7661
|
+
if (!tbrowseVS) return null;
|
|
6080
7662
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
6081
7663
|
children: [
|
|
6082
7664
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
6083
7665
|
className: "gene-genetree",
|
|
6084
7666
|
style: {
|
|
6085
|
-
height: this.
|
|
7667
|
+
height: this.getHeight(),
|
|
6086
7668
|
width: '100%'
|
|
6087
7669
|
},
|
|
6088
7670
|
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$tbrowse.TBrowse), {
|
|
@@ -6096,7 +7678,13 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6096
7678
|
geneStructures: geneStructures,
|
|
6097
7679
|
zones: $64fad37f770d2bfe$var$TBROWSE_ZONES,
|
|
6098
7680
|
nodeOfInterest: this.gene._id,
|
|
6099
|
-
|
|
7681
|
+
viewState: tbrowseVS,
|
|
7682
|
+
onViewStateChange: (next)=>this.setTbrowseViewState(next),
|
|
7683
|
+
defaultOpenSections: {
|
|
7684
|
+
zones: true,
|
|
7685
|
+
search: true
|
|
7686
|
+
},
|
|
7687
|
+
zoneStatus: zoneStatus
|
|
6100
7688
|
})
|
|
6101
7689
|
}),
|
|
6102
7690
|
this.renderResizeHandle()
|
|
@@ -6104,12 +7692,10 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6104
7692
|
});
|
|
6105
7693
|
}
|
|
6106
7694
|
renderViewerToggle() {
|
|
6107
|
-
const
|
|
7695
|
+
const viewer = this.getViewer();
|
|
6108
7696
|
const btn = (id, label)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
|
|
6109
7697
|
type: "button",
|
|
6110
|
-
onClick: ()=>this.
|
|
6111
|
-
viewer: id
|
|
6112
|
-
}),
|
|
7698
|
+
onClick: ()=>this.setViewer(id),
|
|
6113
7699
|
style: {
|
|
6114
7700
|
padding: '4px 10px',
|
|
6115
7701
|
marginRight: 4,
|
|
@@ -6238,7 +7824,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6238
7824
|
else {
|
|
6239
7825
|
const tree = this.props.grameneTrees[treeId];
|
|
6240
7826
|
if (tree.hasOwnProperty('taxon_id')) {
|
|
6241
|
-
this.tree = (0, ($parcel$interopDefault($gXNCa$
|
|
7827
|
+
this.tree = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrcgenetree))).tree([
|
|
6242
7828
|
this.props.grameneTrees[treeId]
|
|
6243
7829
|
]);
|
|
6244
7830
|
this.orthologs = this.orthologList();
|
|
@@ -6253,7 +7839,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6253
7839
|
this.tree && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $5c2c79352d3d7b81$export$7c6e2c02157bb7d2), {
|
|
6254
7840
|
children: [
|
|
6255
7841
|
this.renderViewerToggle(),
|
|
6256
|
-
this.
|
|
7842
|
+
this.getViewer() === 'tbrowse' ? this.renderTBrowse() : this.renderTreeVis()
|
|
6257
7843
|
]
|
|
6258
7844
|
}, "content"),
|
|
6259
7845
|
this.tree && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $5c2c79352d3d7b81$export$900f1bfcd1cb6476), {
|
|
@@ -6266,7 +7852,7 @@ class $64fad37f770d2bfe$var$Homology extends (0, ($parcel$interopDefault($gXNCa$
|
|
|
6266
7852
|
});
|
|
6267
7853
|
}
|
|
6268
7854
|
}
|
|
6269
|
-
var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'doRequestGrameneTree', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
|
|
7855
|
+
var $64fad37f770d2bfe$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneTaxonomy', 'selectGrameneTrees', 'selectGrameneGenomes', 'selectGrameneAPI', 'selectConfiguration', 'selectCuration', 'selectUiViewState', 'doRequestGrameneTree', 'doSetHomologyViewer', 'doSetHomologyHeight', 'doSetHomologyTbrowseViewState', 'doAcceptGrameneSuggestion', 'doReplaceGrameneFilters', $64fad37f770d2bfe$var$Homology);
|
|
6270
7856
|
|
|
6271
7857
|
|
|
6272
7858
|
|
|
@@ -6757,7 +8343,7 @@ function $54c74a4689d5a778$var$buildIframeSrcDoc() {
|
|
|
6757
8343
|
class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
|
|
6758
8344
|
constructor(props){
|
|
6759
8345
|
super(props);
|
|
6760
|
-
this.taxonomy = (0, ($parcel$interopDefault($gXNCa$
|
|
8346
|
+
this.taxonomy = (0, ($parcel$interopDefault($gXNCa$gramenetreesclientsrctaxonomy))).tree(Object.values(props.grameneTaxonomy));
|
|
6761
8347
|
this.gene = props.geneDocs[props.searchResult.id];
|
|
6762
8348
|
this.iframeRef = /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createRef();
|
|
6763
8349
|
// srcDoc is constant — the per-instance pathway/reaction state is
|
|
@@ -8012,22 +9598,55 @@ const $527ebc19dc92444d$var$buildId = (gene, geneSeq, up, down)=>{
|
|
|
8012
9598
|
return `${geneSeq.genome}|${gene._id}|${gene.location.region}:${gs}..${ge} ${extras.join('|')}`;
|
|
8013
9599
|
};
|
|
8014
9600
|
const $527ebc19dc92444d$var$Detail = (props)=>{
|
|
8015
|
-
const
|
|
8016
|
-
const
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
9601
|
+
const geneId = props.searchResult.id;
|
|
9602
|
+
const gene = props.geneDocs[geneId];
|
|
9603
|
+
// Sub-tab + selected isoform + flanking lengths live in the uiViewState
|
|
9604
|
+
// bundle (keyed by geneId) so the shareable-views snapshot can round-trip
|
|
9605
|
+
// them. Defaults match the old local-state initial values.
|
|
9606
|
+
const slice = props.uiViewState && props.uiViewState.byGene[geneId] && props.uiViewState.byGene[geneId].sequences || {};
|
|
9607
|
+
const tab = slice.tab || 'dna';
|
|
9608
|
+
const tid = slice.tid || gene.gene_structure.canonical_transcript;
|
|
9609
|
+
const upstream = slice.upstream || 0;
|
|
9610
|
+
const downstream = slice.downstream || 0;
|
|
9611
|
+
const setTab = (k)=>props.doSetSequencesState({
|
|
9612
|
+
geneId: geneId,
|
|
9613
|
+
patch: {
|
|
9614
|
+
tab: k
|
|
9615
|
+
}
|
|
9616
|
+
});
|
|
9617
|
+
const setTid = (v)=>props.doSetSequencesState({
|
|
9618
|
+
geneId: geneId,
|
|
9619
|
+
patch: {
|
|
9620
|
+
tid: v
|
|
9621
|
+
}
|
|
9622
|
+
});
|
|
9623
|
+
const setUpstream = (v)=>props.doSetSequencesState({
|
|
9624
|
+
geneId: geneId,
|
|
9625
|
+
patch: {
|
|
9626
|
+
upstream: +v
|
|
9627
|
+
}
|
|
9628
|
+
});
|
|
9629
|
+
const setDownstream = (v)=>props.doSetSequencesState({
|
|
9630
|
+
geneId: geneId,
|
|
9631
|
+
patch: {
|
|
9632
|
+
downstream: +v
|
|
9633
|
+
}
|
|
9634
|
+
});
|
|
8020
9635
|
let geneSeq;
|
|
8021
9636
|
let rnaSeq;
|
|
8022
9637
|
let pepSeq;
|
|
8023
|
-
|
|
9638
|
+
// The *_SEQUENCE_REQUESTED reducer inserts a `{}` placeholder before the
|
|
9639
|
+
// fetch resolves, so a key-only existence check passes while the body is
|
|
9640
|
+
// still empty — leading to `seq.substring` of undefined downstream. Gate
|
|
9641
|
+
// on the actual payload field instead.
|
|
9642
|
+
if (props.geneSequences && props.geneSequences[gene._id] && props.geneSequences[gene._id].seq) geneSeq = props.geneSequences[gene._id];
|
|
8024
9643
|
else {
|
|
8025
9644
|
props.doRequestGeneSequence(gene);
|
|
8026
9645
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
|
|
8027
9646
|
children: "loading"
|
|
8028
9647
|
});
|
|
8029
9648
|
}
|
|
8030
|
-
if (props.rnaSequences && props.rnaSequences[tid]) rnaSeq = props.rnaSequences[tid];
|
|
9649
|
+
if (props.rnaSequences && props.rnaSequences[tid] && props.rnaSequences[tid].seq) rnaSeq = props.rnaSequences[tid];
|
|
8031
9650
|
else {
|
|
8032
9651
|
props.doRequestRnaSequence(tid, gene);
|
|
8033
9652
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
|
|
@@ -8040,7 +9659,7 @@ const $527ebc19dc92444d$var$Detail = (props)=>{
|
|
|
8040
9659
|
let tl_id;
|
|
8041
9660
|
if (transcript.translation) {
|
|
8042
9661
|
tl_id = transcript.translation.id;
|
|
8043
|
-
if (props.pepSequences && props.pepSequences[tl_id]) pepSeq = props.pepSequences[tl_id];
|
|
9662
|
+
if (props.pepSequences && props.pepSequences[tl_id] && props.pepSequences[tl_id].seq) pepSeq = props.pepSequences[tl_id];
|
|
8044
9663
|
else {
|
|
8045
9664
|
props.doRequestPepSequence(tl_id, gene);
|
|
8046
9665
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
|
|
@@ -8257,7 +9876,7 @@ const $527ebc19dc92444d$var$Detail = (props)=>{
|
|
|
8257
9876
|
]
|
|
8258
9877
|
});
|
|
8259
9878
|
};
|
|
8260
|
-
var $527ebc19dc92444d$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGeneSequences', 'selectRnaSequences', 'selectPepSequences', 'doRequestGeneSequence', 'doRequestRnaSequence', 'doRequestPepSequence', $527ebc19dc92444d$var$Detail);
|
|
9879
|
+
var $527ebc19dc92444d$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGeneSequences', 'selectRnaSequences', 'selectPepSequences', 'selectUiViewState', 'doRequestGeneSequence', 'doRequestRnaSequence', 'doRequestPepSequence', 'doSetSequencesState', $527ebc19dc92444d$var$Detail);
|
|
8261
9880
|
|
|
8262
9881
|
|
|
8263
9882
|
|
|
@@ -8447,12 +10066,12 @@ const $6c5c4f90059875bf$var$allDetails = [
|
|
|
8447
10066
|
class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$react))).Component {
|
|
8448
10067
|
constructor(props){
|
|
8449
10068
|
super(props);
|
|
10069
|
+
// `details` is derived per-render config (which tabs the site enables,
|
|
10070
|
+
// crossed with this row's `capabilities`) — keep it local.
|
|
8450
10071
|
this.state = {
|
|
8451
10072
|
details: $6c5c4f90059875bf$var$allDetails.map((o)=>({
|
|
8452
10073
|
...o
|
|
8453
|
-
})).filter((d)=>props.config.details[d.id])
|
|
8454
|
-
expandedDetail: props.expandedDetail,
|
|
8455
|
-
fullscreen: false
|
|
10074
|
+
})).filter((d)=>props.config.details[d.id])
|
|
8456
10075
|
};
|
|
8457
10076
|
let hasData = {};
|
|
8458
10077
|
props.searchResult.capabilities.forEach((c)=>{
|
|
@@ -8461,28 +10080,46 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
|
|
|
8461
10080
|
});
|
|
8462
10081
|
this.state.details.forEach((d)=>d.available |= hasData.hasOwnProperty(d.id));
|
|
8463
10082
|
}
|
|
10083
|
+
componentDidMount() {
|
|
10084
|
+
// Seed the auto-open-homology intent (from GeneList when numFound===1) the
|
|
10085
|
+
// first time this row mounts, only if the user hasn't already opened/closed
|
|
10086
|
+
// a tab here. Without this, the bundle would have no entry and the prior
|
|
10087
|
+
// "auto-expand homology on single-result" behavior would be lost.
|
|
10088
|
+
if (this.props.expandedDetail && this.expandedDetail === undefined) this.props.doExpandGeneDetail({
|
|
10089
|
+
geneId: this.props.searchResult.id,
|
|
10090
|
+
detail: this.props.expandedDetail
|
|
10091
|
+
});
|
|
10092
|
+
}
|
|
10093
|
+
get expandedDetail() {
|
|
10094
|
+
const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.props.searchResult.id];
|
|
10095
|
+
return slice ? slice.expandedDetail : undefined;
|
|
10096
|
+
}
|
|
10097
|
+
get fullscreen() {
|
|
10098
|
+
const slice = this.props.uiViewState && this.props.uiViewState.byGene[this.props.searchResult.id];
|
|
10099
|
+
return !!(slice && slice.fullscreen);
|
|
10100
|
+
}
|
|
8464
10101
|
getDetailStatus(d) {
|
|
8465
|
-
if (this.
|
|
10102
|
+
if (this.expandedDetail === d.id) return 'expanded';
|
|
8466
10103
|
if (d.available) return 'closed';
|
|
8467
10104
|
return d.id === "pubs" ? 'empty' : 'disabled';
|
|
8468
10105
|
}
|
|
8469
10106
|
setExpanded(d) {
|
|
8470
10107
|
if (d.available || d.id === "pubs") {
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
10108
|
+
const geneId = this.props.searchResult.id;
|
|
10109
|
+
if (this.expandedDetail === d.id) this.props.doExpandGeneDetail({
|
|
10110
|
+
geneId: geneId,
|
|
10111
|
+
detail: null
|
|
8474
10112
|
});
|
|
8475
10113
|
else {
|
|
8476
|
-
const geneId = this.props.searchResult.id;
|
|
8477
10114
|
if (!(this.props.geneDocs && this.props.geneDocs.hasOwnProperty(geneId))) this.props.requestGene(geneId);
|
|
8478
10115
|
(0, ($parcel$interopDefault($gXNCa$reactga4))).event({
|
|
8479
10116
|
category: 'Search',
|
|
8480
10117
|
action: 'Details',
|
|
8481
10118
|
label: d.label
|
|
8482
10119
|
});
|
|
8483
|
-
this.
|
|
8484
|
-
|
|
8485
|
-
|
|
10120
|
+
this.props.doExpandGeneDetail({
|
|
10121
|
+
geneId: geneId,
|
|
10122
|
+
detail: d.id
|
|
8486
10123
|
});
|
|
8487
10124
|
}
|
|
8488
10125
|
}
|
|
@@ -8560,7 +10197,7 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
|
|
|
8560
10197
|
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
8561
10198
|
className: "gene-detail-tabs",
|
|
8562
10199
|
children: this.state.details.map((d, idx)=>{
|
|
8563
|
-
const isExpanded = this.
|
|
10200
|
+
const isExpanded = this.expandedDetail === d.id;
|
|
8564
10201
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.OverlayTrigger), {
|
|
8565
10202
|
placement: 'bottom',
|
|
8566
10203
|
overlay: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tooltip), {
|
|
@@ -8586,7 +10223,8 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
|
|
|
8586
10223
|
title: "View full screen",
|
|
8587
10224
|
onClick: (e)=>{
|
|
8588
10225
|
e.stopPropagation();
|
|
8589
|
-
this.
|
|
10226
|
+
this.props.doSetGeneFullscreen({
|
|
10227
|
+
geneId: searchResult.id,
|
|
8590
10228
|
fullscreen: true
|
|
8591
10229
|
});
|
|
8592
10230
|
}
|
|
@@ -8596,19 +10234,24 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
|
|
|
8596
10234
|
}, idx);
|
|
8597
10235
|
})
|
|
8598
10236
|
}),
|
|
8599
|
-
this.
|
|
10237
|
+
this.expandedDetail && this.ensureGene(searchResult.id) && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $5c2c79352d3d7b81$export$ec6b54b688be1708), {
|
|
8600
10238
|
className: "visible-detail",
|
|
8601
|
-
fullscreen: this.
|
|
8602
|
-
onExitFullscreen: ()=>this.
|
|
10239
|
+
fullscreen: this.fullscreen,
|
|
10240
|
+
onExitFullscreen: ()=>this.props.doSetGeneFullscreen({
|
|
10241
|
+
geneId: searchResult.id,
|
|
8603
10242
|
fullscreen: false
|
|
8604
10243
|
}),
|
|
8605
|
-
title: (this.state.details.find((d)=>d.id === this.
|
|
8606
|
-
children: /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createElement($6c5c4f90059875bf$var$inventory[this.
|
|
10244
|
+
title: (this.state.details.find((d)=>d.id === this.expandedDetail) || {}).label,
|
|
10245
|
+
children: /*#__PURE__*/ (0, ($parcel$interopDefault($gXNCa$react))).createElement($6c5c4f90059875bf$var$inventory[this.expandedDetail], this.props)
|
|
8607
10246
|
})
|
|
8608
10247
|
]
|
|
8609
10248
|
});
|
|
8610
10249
|
}
|
|
8611
10250
|
}
|
|
10251
|
+
// Wrap Gene so each row gets uiViewState + dispatchers. (We don't pull these
|
|
10252
|
+
// in at the GeneList level because the slice we care about is per-geneId; the
|
|
10253
|
+
// component-side getters read by row id.)
|
|
10254
|
+
const $6c5c4f90059875bf$var$ConnectedGene = (0, $gXNCa$reduxbundlerreact.connect)('selectUiViewState', 'doExpandGeneDetail', 'doSetGeneFullscreen', $6c5c4f90059875bf$var$Gene);
|
|
8612
10255
|
const $6c5c4f90059875bf$var$GeneList = (props)=>{
|
|
8613
10256
|
if (props.grameneSearch && props.grameneSearch.response && props.grameneTaxonomy) {
|
|
8614
10257
|
let prev, page, next;
|
|
@@ -8650,7 +10293,7 @@ const $6c5c4f90059875bf$var$GeneList = (props)=>{
|
|
|
8650
10293
|
next
|
|
8651
10294
|
]
|
|
8652
10295
|
}),
|
|
8653
|
-
props.grameneSearch.response.docs.map((g, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($6c5c4f90059875bf$var$
|
|
10296
|
+
props.grameneSearch.response.docs.map((g, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($6c5c4f90059875bf$var$ConnectedGene, {
|
|
8654
10297
|
searchResult: g,
|
|
8655
10298
|
config: props.configuration,
|
|
8656
10299
|
taxName: props.grameneTaxonomy[g.taxon_id].name,
|
|
@@ -12864,12 +14507,19 @@ function $caf32827df861c4e$var$logTickFormat(v) {
|
|
|
12864
14507
|
if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
|
|
12865
14508
|
return $gXNCa$d3.format('.0e')(v);
|
|
12866
14509
|
}
|
|
12867
|
-
const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
|
|
14510
|
+
const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', initialSelections: initialSelections = null, onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null, axisLabels: axisLabels = null })=>{
|
|
12868
14511
|
const svgRef = (0, $gXNCa$react.useRef)(null);
|
|
12869
14512
|
const containerRef = (0, $gXNCa$react.useRef)(null);
|
|
12870
14513
|
// selections in data domain: { [field]: [lo, hi] }
|
|
12871
14514
|
const selectionsRef = (0, $gXNCa$react.useRef)({});
|
|
12872
14515
|
const lastClearRef = (0, $gXNCa$react.useRef)(0);
|
|
14516
|
+
// Seed brushes from a restored saved view exactly once (the first render
|
|
14517
|
+
// where `initialSelections` is non-empty). Read via a ref so a change to the
|
|
14518
|
+
// prop — which happens on every brush as it round-trips through the store —
|
|
14519
|
+
// doesn't force an SVG rebuild; the deps below already rerun on data changes.
|
|
14520
|
+
const initialSelectionsRef = (0, $gXNCa$react.useRef)(initialSelections);
|
|
14521
|
+
initialSelectionsRef.current = initialSelections;
|
|
14522
|
+
const seededRef = (0, $gXNCa$react.useRef)(false);
|
|
12873
14523
|
// Track container size so the d3 render reruns when the user drags the
|
|
12874
14524
|
// pane resizer (or when the window is resized). The values themselves
|
|
12875
14525
|
// aren't read inside the effect — the effect always reads clientWidth/
|
|
@@ -12900,6 +14550,15 @@ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields,
|
|
|
12900
14550
|
return ()=>ro.disconnect();
|
|
12901
14551
|
}, []);
|
|
12902
14552
|
(0, $gXNCa$react.useEffect)(()=>{
|
|
14553
|
+
if (!seededRef.current) {
|
|
14554
|
+
const init = initialSelectionsRef.current;
|
|
14555
|
+
if (init && Object.keys(init).length) {
|
|
14556
|
+
selectionsRef.current = {
|
|
14557
|
+
...init
|
|
14558
|
+
};
|
|
14559
|
+
seededRef.current = true;
|
|
14560
|
+
}
|
|
14561
|
+
}
|
|
12903
14562
|
if (clearVersion !== lastClearRef.current) {
|
|
12904
14563
|
selectionsRef.current = {};
|
|
12905
14564
|
lastClearRef.current = clearVersion;
|
|
@@ -13767,7 +15426,10 @@ const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
|
|
|
13767
15426
|
onOpenFields: ()=>doToggleExprVizFieldsModal(tid, true),
|
|
13768
15427
|
onLoad: ()=>doFetchExprVizData(tid),
|
|
13769
15428
|
onReorder: (next)=>props.doReorderExprVizFields(tid, next),
|
|
13770
|
-
onAddRangeQuery: props.doAddGrameneRangeQuery
|
|
15429
|
+
onAddRangeQuery: props.doAddGrameneRangeQuery,
|
|
15430
|
+
onSetVizMode: (m)=>props.doSetExprVizVizMode(tid, m),
|
|
15431
|
+
onSetScale: (s)=>props.doSetExprVizScale(tid, s),
|
|
15432
|
+
onSetBrushes: (b)=>props.doSetExprVizBrushes(tid, b)
|
|
13771
15433
|
})
|
|
13772
15434
|
}, tid);
|
|
13773
15435
|
})
|
|
@@ -13927,16 +15589,21 @@ function $1fd2507769d5bd00$var$downloadTsv(filename, rows, fields, studies, expr
|
|
|
13927
15589
|
document.body.removeChild(a);
|
|
13928
15590
|
URL.revokeObjectURL(url);
|
|
13929
15591
|
}
|
|
13930
|
-
const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery })=>{
|
|
15592
|
+
const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery, onSetVizMode: onSetVizMode, onSetScale: onSetScale, onSetBrushes: onSetBrushes })=>{
|
|
13931
15593
|
const selected = tabState && tabState.selectedFields || [];
|
|
13932
15594
|
const rows = tabState && tabState.rows || [];
|
|
13933
15595
|
const fetchInfo = tabState && tabState.fetch || {
|
|
13934
15596
|
status: 'idle',
|
|
13935
15597
|
total: 0
|
|
13936
15598
|
};
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
const
|
|
15599
|
+
// View config (sub-tab, scale, brushes) is persisted in the exprViz bundle
|
|
15600
|
+
// per taxon so a saved view can round-trip it. Driven controlled from here.
|
|
15601
|
+
const scale = tabState && tabState.scale || 'linear';
|
|
15602
|
+
const vizMode = tabState && tabState.vizMode || 'heatmap'; // 'parallel' | 'heatmap'
|
|
15603
|
+
const selections = tabState && tabState.brushes || {};
|
|
15604
|
+
const setScale = (s)=>onSetScale && onSetScale(s);
|
|
15605
|
+
const setVizMode = (m)=>onSetVizMode && onSetVizMode(m);
|
|
15606
|
+
const setSelections = (b)=>onSetBrushes && onSetBrushes(b);
|
|
13940
15607
|
const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
|
|
13941
15608
|
const [hoveredId, setHoveredId] = (0, $gXNCa$react.useState)(null);
|
|
13942
15609
|
const [plotHeight, setPlotHeight] = (0, $gXNCa$react.useState)(320);
|
|
@@ -14012,15 +15679,6 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
|
|
|
14012
15679
|
studies,
|
|
14013
15680
|
expressionSamples
|
|
14014
15681
|
]);
|
|
14015
|
-
(0, $gXNCa$react.useEffect)(()=>{
|
|
14016
|
-
if (rows.length === 0 && hasBrush) {
|
|
14017
|
-
setSelections({});
|
|
14018
|
-
setClearVersion((v)=>v + 1);
|
|
14019
|
-
}
|
|
14020
|
-
}, [
|
|
14021
|
-
rows.length,
|
|
14022
|
-
hasBrush
|
|
14023
|
-
]);
|
|
14024
15682
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
14025
15683
|
className: "exprviz-tab-panel",
|
|
14026
15684
|
children: [
|
|
@@ -14161,6 +15819,7 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
|
|
|
14161
15819
|
rows: rows,
|
|
14162
15820
|
fields: visibleFields,
|
|
14163
15821
|
scale: scale,
|
|
15822
|
+
initialSelections: selections,
|
|
14164
15823
|
onBrushChange: setSelections,
|
|
14165
15824
|
onReorder: handleReorder,
|
|
14166
15825
|
clearVersion: clearVersion,
|
|
@@ -14193,7 +15852,7 @@ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expr
|
|
|
14193
15852
|
]
|
|
14194
15853
|
});
|
|
14195
15854
|
};
|
|
14196
|
-
var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
|
|
15855
|
+
var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doSetExprVizVizMode', 'doSetExprVizScale', 'doSetExprVizBrushes', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
|
|
14197
15856
|
|
|
14198
15857
|
|
|
14199
15858
|
|
|
@@ -14682,7 +16341,7 @@ const $597fe213417ee6ca$var$SortableTh = ({ label: label, sortKey: sortKey, acti
|
|
|
14682
16341
|
}) : inner
|
|
14683
16342
|
});
|
|
14684
16343
|
};
|
|
14685
|
-
const $597fe213417ee6ca$var$OntologySection = ({ block: block, search: search, onAddFilter: onAddFilter })=>{
|
|
16344
|
+
const $597fe213417ee6ca$var$OntologySection = ({ block: block, search: search, sort: sort, onSortChange: onSortChange, onAddFilter: onAddFilter })=>{
|
|
14686
16345
|
const filtered = (0, $gXNCa$react.useMemo)(()=>{
|
|
14687
16346
|
if (!search) return block.rows;
|
|
14688
16347
|
const needle = search.toLowerCase();
|
|
@@ -14692,14 +16351,13 @@ const $597fe213417ee6ca$var$OntologySection = ({ block: block, search: search, o
|
|
|
14692
16351
|
search
|
|
14693
16352
|
]);
|
|
14694
16353
|
const showType = $597fe213417ee6ca$var$ONTS_WITH_TYPE_COLUMN.has(block.ontology);
|
|
14695
|
-
|
|
14696
|
-
|
|
16354
|
+
// Sort state is persisted per section in the bundle ui so a saved view
|
|
16355
|
+
// restores it. Defaults match the historical local-state initials.
|
|
16356
|
+
const sortKey = sort && sort.key || 'pAdj';
|
|
16357
|
+
const sortDir = sort && sort.dir || 'asc';
|
|
14697
16358
|
const handleSort = (key)=>{
|
|
14698
|
-
if (key === sortKey)
|
|
14699
|
-
else
|
|
14700
|
-
setSortKey(key);
|
|
14701
|
-
setSortDir($597fe213417ee6ca$var$SORT_DEFAULT_DIR[key] || 'asc');
|
|
14702
|
-
}
|
|
16359
|
+
if (key === sortKey) onSortChange(block.ontology, sortKey, sortDir === 'asc' ? 'desc' : 'asc');
|
|
16360
|
+
else onSortChange(block.ontology, key, $597fe213417ee6ca$var$SORT_DEFAULT_DIR[key] || 'asc');
|
|
14703
16361
|
};
|
|
14704
16362
|
const sorted = (0, $gXNCa$react.useMemo)(()=>{
|
|
14705
16363
|
const accessor = $597fe213417ee6ca$var$SORT_ACCESSORS[sortKey];
|
|
@@ -14896,6 +16554,17 @@ const $597fe213417ee6ca$var$TaxonPanel = ({ taxon: taxon, ontologyEnrichment: on
|
|
|
14896
16554
|
] : [];
|
|
14897
16555
|
// Hide ontologies that aren't used in this species at all.
|
|
14898
16556
|
const blocks = allBlocks.filter((b)=>b.tested > 0);
|
|
16557
|
+
const handleSortChange = (sectionKey, key, dir)=>{
|
|
16558
|
+
onUiChange({
|
|
16559
|
+
sort: {
|
|
16560
|
+
...ui.sort || {},
|
|
16561
|
+
[sectionKey]: {
|
|
16562
|
+
key: key,
|
|
16563
|
+
dir: dir
|
|
16564
|
+
}
|
|
16565
|
+
}
|
|
16566
|
+
});
|
|
16567
|
+
};
|
|
14899
16568
|
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
14900
16569
|
className: "oe-panel",
|
|
14901
16570
|
children: [
|
|
@@ -14923,6 +16592,8 @@ const $597fe213417ee6ca$var$TaxonPanel = ({ taxon: taxon, ontologyEnrichment: on
|
|
|
14923
16592
|
children: blocks.map((b)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($597fe213417ee6ca$var$OntologySection, {
|
|
14924
16593
|
block: b,
|
|
14925
16594
|
search: ui.search,
|
|
16595
|
+
sort: ui.sort && ui.sort[b.ontology],
|
|
16596
|
+
onSortChange: handleSortChange,
|
|
14926
16597
|
onAddFilter: onAddFilter
|
|
14927
16598
|
}, b.ontology))
|
|
14928
16599
|
})
|
|
@@ -15061,6 +16732,223 @@ var $597fe213417ee6ca$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
|
|
|
15061
16732
|
|
|
15062
16733
|
|
|
15063
16734
|
|
|
16735
|
+
// "Save this view" UI — sits inside the Auth sidebar panel.
|
|
16736
|
+
//
|
|
16737
|
+
// Sign-in-gated by Auth.js (we only mount this when `user` is truthy). Opens
|
|
16738
|
+
// a modal with label + description + visibility toggle; on save, swaps to a
|
|
16739
|
+
// post-save state that shows the shareable URL with a copy button.
|
|
16740
|
+
//
|
|
16741
|
+
// Talks to the savedViews bundle. The snapshot itself is built inside
|
|
16742
|
+
// doSaveView via selectViewSnapshot — this component doesn't know or care
|
|
16743
|
+
// what the snapshot looks like.
|
|
16744
|
+
|
|
16745
|
+
|
|
16746
|
+
|
|
16747
|
+
|
|
16748
|
+
|
|
16749
|
+
const $e63a3bee79bd1c7a$var$SaveViewButtonCmp = ({ user: user, savedViews: savedViews, doSaveView: doSaveView, doResetSavedViewState: doResetSavedViewState })=>{
|
|
16750
|
+
const [showModal, setShowModal] = (0, $gXNCa$react.useState)(false);
|
|
16751
|
+
const [label, setLabel] = (0, $gXNCa$react.useState)('');
|
|
16752
|
+
const [description, setDescription] = (0, $gXNCa$react.useState)('');
|
|
16753
|
+
const [isPublic, setIsPublic] = (0, $gXNCa$react.useState)(false);
|
|
16754
|
+
const [shareUrl, setShareUrl] = (0, $gXNCa$react.useState)(null);
|
|
16755
|
+
const [copied, setCopied] = (0, $gXNCa$react.useState)(false);
|
|
16756
|
+
const reset = (0, $gXNCa$react.useCallback)(()=>{
|
|
16757
|
+
setLabel('');
|
|
16758
|
+
setDescription('');
|
|
16759
|
+
setIsPublic(false);
|
|
16760
|
+
setShareUrl(null);
|
|
16761
|
+
setCopied(false);
|
|
16762
|
+
doResetSavedViewState();
|
|
16763
|
+
}, [
|
|
16764
|
+
doResetSavedViewState
|
|
16765
|
+
]);
|
|
16766
|
+
const handleOpen = ()=>{
|
|
16767
|
+
reset();
|
|
16768
|
+
setShowModal(true);
|
|
16769
|
+
};
|
|
16770
|
+
const handleClose = ()=>{
|
|
16771
|
+
setShowModal(false);
|
|
16772
|
+
reset();
|
|
16773
|
+
};
|
|
16774
|
+
const handleSave = async (e)=>{
|
|
16775
|
+
e.preventDefault();
|
|
16776
|
+
if (!label.trim()) return;
|
|
16777
|
+
try {
|
|
16778
|
+
const { shareUrl: shareUrl } = await doSaveView({
|
|
16779
|
+
user: user,
|
|
16780
|
+
label: label.trim(),
|
|
16781
|
+
description: description.trim(),
|
|
16782
|
+
isPublic: isPublic
|
|
16783
|
+
});
|
|
16784
|
+
setShareUrl(shareUrl);
|
|
16785
|
+
} catch (_) {
|
|
16786
|
+
// savedViews.saveError is already set; modal renders it
|
|
16787
|
+
}
|
|
16788
|
+
};
|
|
16789
|
+
const handleCopy = async ()=>{
|
|
16790
|
+
if (!shareUrl) return;
|
|
16791
|
+
try {
|
|
16792
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
16793
|
+
setCopied(true);
|
|
16794
|
+
setTimeout(()=>setCopied(false), 1500);
|
|
16795
|
+
} catch (_) {}
|
|
16796
|
+
};
|
|
16797
|
+
const saving = savedViews && savedViews.saving;
|
|
16798
|
+
const saveError = savedViews && savedViews.saveError;
|
|
16799
|
+
return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
16800
|
+
children: [
|
|
16801
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
16802
|
+
size: "sm",
|
|
16803
|
+
variant: "outline-primary",
|
|
16804
|
+
style: {
|
|
16805
|
+
marginTop: 8
|
|
16806
|
+
},
|
|
16807
|
+
onClick: handleOpen,
|
|
16808
|
+
title: "Save the current filters, views, and detail tabs as a shareable link",
|
|
16809
|
+
children: "Save this view"
|
|
16810
|
+
}),
|
|
16811
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal), {
|
|
16812
|
+
show: showModal,
|
|
16813
|
+
onHide: handleClose,
|
|
16814
|
+
centered: true,
|
|
16815
|
+
children: [
|
|
16816
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Modal).Header, {
|
|
16817
|
+
closeButton: true,
|
|
16818
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Modal).Title, {
|
|
16819
|
+
children: shareUrl ? 'View saved' : 'Save this view'
|
|
16820
|
+
})
|
|
16821
|
+
}),
|
|
16822
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal).Body, {
|
|
16823
|
+
children: [
|
|
16824
|
+
!shareUrl && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form), {
|
|
16825
|
+
onSubmit: handleSave,
|
|
16826
|
+
children: [
|
|
16827
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
|
|
16828
|
+
className: "mb-3",
|
|
16829
|
+
children: [
|
|
16830
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
|
|
16831
|
+
children: "Name"
|
|
16832
|
+
}),
|
|
16833
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
|
|
16834
|
+
autoFocus: true,
|
|
16835
|
+
type: "text",
|
|
16836
|
+
placeholder: "e.g. TAIR loci with TF binding sites",
|
|
16837
|
+
value: label,
|
|
16838
|
+
onChange: (e)=>setLabel(e.target.value),
|
|
16839
|
+
disabled: saving,
|
|
16840
|
+
required: true
|
|
16841
|
+
})
|
|
16842
|
+
]
|
|
16843
|
+
}),
|
|
16844
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
|
|
16845
|
+
className: "mb-3",
|
|
16846
|
+
children: [
|
|
16847
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Label, {
|
|
16848
|
+
children: "Description (optional)"
|
|
16849
|
+
}),
|
|
16850
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
|
|
16851
|
+
as: "textarea",
|
|
16852
|
+
rows: 2,
|
|
16853
|
+
value: description,
|
|
16854
|
+
onChange: (e)=>setDescription(e.target.value),
|
|
16855
|
+
disabled: saving
|
|
16856
|
+
})
|
|
16857
|
+
]
|
|
16858
|
+
}),
|
|
16859
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Group, {
|
|
16860
|
+
className: "mb-3",
|
|
16861
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Check, {
|
|
16862
|
+
type: "switch",
|
|
16863
|
+
id: "save-view-public",
|
|
16864
|
+
label: "Make this view public (visible to anyone with the link)",
|
|
16865
|
+
checked: isPublic,
|
|
16866
|
+
onChange: (e)=>setIsPublic(e.target.checked),
|
|
16867
|
+
disabled: saving
|
|
16868
|
+
})
|
|
16869
|
+
}),
|
|
16870
|
+
saveError && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Alert), {
|
|
16871
|
+
variant: "danger",
|
|
16872
|
+
children: saveError
|
|
16873
|
+
}),
|
|
16874
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
16875
|
+
className: "d-flex justify-content-end gap-2",
|
|
16876
|
+
children: [
|
|
16877
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
16878
|
+
variant: "secondary",
|
|
16879
|
+
onClick: handleClose,
|
|
16880
|
+
disabled: saving,
|
|
16881
|
+
children: "Cancel"
|
|
16882
|
+
}),
|
|
16883
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
16884
|
+
type: "submit",
|
|
16885
|
+
variant: "primary",
|
|
16886
|
+
disabled: saving || !label.trim(),
|
|
16887
|
+
children: saving ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
16888
|
+
children: [
|
|
16889
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Spinner), {
|
|
16890
|
+
as: "span",
|
|
16891
|
+
size: "sm",
|
|
16892
|
+
animation: "border"
|
|
16893
|
+
}),
|
|
16894
|
+
" Saving\u2026"
|
|
16895
|
+
]
|
|
16896
|
+
}) : 'Save'
|
|
16897
|
+
})
|
|
16898
|
+
]
|
|
16899
|
+
})
|
|
16900
|
+
]
|
|
16901
|
+
}),
|
|
16902
|
+
shareUrl && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
16903
|
+
children: [
|
|
16904
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("p", {
|
|
16905
|
+
children: "Share this link to let others open the same search and detail state:"
|
|
16906
|
+
}),
|
|
16907
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.InputGroup), {
|
|
16908
|
+
children: [
|
|
16909
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
|
|
16910
|
+
type: "text",
|
|
16911
|
+
value: shareUrl,
|
|
16912
|
+
readOnly: true,
|
|
16913
|
+
onFocus: (e)=>e.target.select()
|
|
16914
|
+
}),
|
|
16915
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
16916
|
+
variant: copied ? 'success' : 'outline-secondary',
|
|
16917
|
+
onClick: handleCopy,
|
|
16918
|
+
children: copied ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
16919
|
+
children: [
|
|
16920
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsCheck2), {}),
|
|
16921
|
+
" Copied"
|
|
16922
|
+
]
|
|
16923
|
+
}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
|
|
16924
|
+
children: [
|
|
16925
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsClipboard), {}),
|
|
16926
|
+
" Copy"
|
|
16927
|
+
]
|
|
16928
|
+
})
|
|
16929
|
+
})
|
|
16930
|
+
]
|
|
16931
|
+
}),
|
|
16932
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
16933
|
+
className: "mt-3 d-flex justify-content-end",
|
|
16934
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
16935
|
+
variant: "primary",
|
|
16936
|
+
onClick: handleClose,
|
|
16937
|
+
children: "Done"
|
|
16938
|
+
})
|
|
16939
|
+
})
|
|
16940
|
+
]
|
|
16941
|
+
})
|
|
16942
|
+
]
|
|
16943
|
+
})
|
|
16944
|
+
]
|
|
16945
|
+
})
|
|
16946
|
+
]
|
|
16947
|
+
});
|
|
16948
|
+
};
|
|
16949
|
+
var $e63a3bee79bd1c7a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectSavedViews', 'doSaveView', 'doResetSavedViewState', $e63a3bee79bd1c7a$var$SaveViewButtonCmp);
|
|
16950
|
+
|
|
16951
|
+
|
|
15064
16952
|
const $c5d403787de8b05f$var$provider = new (0, $gXNCa$firebaseauth.GoogleAuthProvider)();
|
|
15065
16953
|
const $c5d403787de8b05f$var$Auth = (props)=>{
|
|
15066
16954
|
const firebaseConfig = props.configuration && props.configuration.firebaseConfig;
|
|
@@ -15070,10 +16958,25 @@ const $c5d403787de8b05f$var$Auth = (props)=>{
|
|
|
15070
16958
|
}, [
|
|
15071
16959
|
firebaseConfig
|
|
15072
16960
|
]);
|
|
15073
|
-
const [user, setUser] = (0, $gXNCa$react.useState)(
|
|
16961
|
+
const [user, setUser] = (0, $gXNCa$react.useState)(null);
|
|
15074
16962
|
const [open, setOpen] = (0, $gXNCa$react.useState)(true);
|
|
16963
|
+
// Subscribe once, unsubscribe on unmount. On each auth-state emission,
|
|
16964
|
+
// also poke the shared-view boot path so a `?view=<hash>` for a private
|
|
16965
|
+
// view gets retried with the now-available Bearer token. bootViewFromUrl
|
|
16966
|
+
// no-ops when the param isn't present, so this is cheap.
|
|
16967
|
+
(0, $gXNCa$react.useEffect)(()=>{
|
|
16968
|
+
if (!auth) return undefined;
|
|
16969
|
+
const unsub = (0, $gXNCa$firebaseauth.onAuthStateChanged)(auth, (u)=>{
|
|
16970
|
+
setUser(u);
|
|
16971
|
+
props.doBootSharedView({
|
|
16972
|
+
user: u
|
|
16973
|
+
});
|
|
16974
|
+
});
|
|
16975
|
+
return unsub;
|
|
16976
|
+
}, [
|
|
16977
|
+
auth
|
|
16978
|
+
]);
|
|
15075
16979
|
if (!auth) return null;
|
|
15076
|
-
(0, $gXNCa$firebaseauth.onAuthStateChanged)(auth, (user)=>setUser(user));
|
|
15077
16980
|
function handleLogin() {
|
|
15078
16981
|
(0, $gXNCa$firebaseauth.signInWithPopup)(auth, $c5d403787de8b05f$var$provider).then((result)=>{
|
|
15079
16982
|
setUser(result.user);
|
|
@@ -15109,27 +17012,34 @@ const $c5d403787de8b05f$var$Auth = (props)=>{
|
|
|
15109
17012
|
})
|
|
15110
17013
|
]
|
|
15111
17014
|
}),
|
|
15112
|
-
open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.
|
|
17015
|
+
open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
|
|
15113
17016
|
className: "sidebar-section-body",
|
|
15114
|
-
children:
|
|
15115
|
-
|
|
15116
|
-
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15120
|
-
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
17017
|
+
children: [
|
|
17018
|
+
/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
17019
|
+
children: user ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
17020
|
+
size: "sm",
|
|
17021
|
+
variant: "success",
|
|
17022
|
+
onClick: handleLogout,
|
|
17023
|
+
children: user.displayName
|
|
17024
|
+
}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
|
|
17025
|
+
size: "sm",
|
|
17026
|
+
variant: "success",
|
|
17027
|
+
onClick: handleLogin,
|
|
17028
|
+
children: "Login"
|
|
17029
|
+
})
|
|
17030
|
+
}),
|
|
17031
|
+
user && user.uid && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
|
|
17032
|
+
children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $e63a3bee79bd1c7a$export$2e2bcd8739ae039), {
|
|
17033
|
+
user: user
|
|
17034
|
+
})
|
|
15125
17035
|
})
|
|
15126
|
-
|
|
17036
|
+
]
|
|
15127
17037
|
})
|
|
15128
17038
|
]
|
|
15129
17039
|
})
|
|
15130
17040
|
});
|
|
15131
17041
|
};
|
|
15132
|
-
var $c5d403787de8b05f$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', $c5d403787de8b05f$var$Auth);
|
|
17042
|
+
var $c5d403787de8b05f$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'doBootSharedView', $c5d403787de8b05f$var$Auth);
|
|
15133
17043
|
|
|
15134
17044
|
|
|
15135
17045
|
|
|
@@ -15505,4 +17415,5 @@ const $693dd8c7a5607c3a$export$5cb791131c501f6a = (0, $gXNCa$reduxbundlerreact.c
|
|
|
15505
17415
|
|
|
15506
17416
|
|
|
15507
17417
|
|
|
17418
|
+
|
|
15508
17419
|
//# sourceMappingURL=index.js.map
|