gramene-search 2.18.3 → 2.20.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/dist/index.js CHANGED
@@ -370,6 +370,50 @@ $9d9aeaf9299e61a1$var$grameneMaps.reactGrameneMaps = (0, $gXNCa$reduxbundler.cre
370
370
  actionCreator: 'doFetchGrameneMaps'
371
371
  };
372
372
  });
373
+ /**
374
+ * The site's reference genome, derived from /maps rather than configured per site.
375
+ *
376
+ * `is_anchor` marks the reference genome of a species group and `anchor_taxon_id`
377
+ * is that group's bare species id. Note the two are deliberately different scales:
378
+ * an anchor's own taxon_id is genome-level (4558006) and never equals its
379
+ * anchor_taxon_id (4558), so the anchor is found by group, not by matching ids.
380
+ *
381
+ * A deployment is whichever group it carries the most genomes for — sorghum_v11
382
+ * has 120 of 128 genomes under 4558, whose anchor is 4558006 (Sb bicolor BTx623
383
+ * v3). Deriving it this way keeps every site correct across releases; the value
384
+ * that used to be configured had drifted to 4558001, a non-anchor wild relative
385
+ * (Sb verticilliflorum 353).
386
+ *
387
+ * Returns null in two cases callers must tolerate: before /maps has loaded, and
388
+ * on a release whose /maps carries no anchor fields at all — gramene main is like
389
+ * that today (114 genomes, not one with is_anchor), so that deployment simply has
390
+ * no reference genome rather than a wrong one.
391
+ *
392
+ * Verified across deployments: sorghum 4558006 (Sb bicolor BTx623 v3),
393
+ * oryza 39947013 and maize 4577003 (both Nipponbare / B73 v5). The genome-level
394
+ * id differs per release, which is the reason to resolve it rather than pin it.
395
+ */ $9d9aeaf9299e61a1$var$grameneMaps.selectTargetTaxonId = (0, $gXNCa$reduxbundler.createSelector)('selectGrameneMaps', (grameneMaps)=>{
396
+ if (!grameneMaps) return null;
397
+ const genomes = Object.values(grameneMaps);
398
+ if (!genomes.length) return null;
399
+ const perGroup = {};
400
+ genomes.forEach((g)=>{
401
+ if (!g || g.anchor_taxon_id === undefined || g.anchor_taxon_id === null) return;
402
+ const k = String(g.anchor_taxon_id);
403
+ perGroup[k] = (perGroup[k] || 0) + 1;
404
+ });
405
+ let group = null;
406
+ let best = -1;
407
+ Object.keys(perGroup).forEach((k)=>{
408
+ if (perGroup[k] > best) {
409
+ best = perGroup[k];
410
+ group = k;
411
+ }
412
+ });
413
+ if (group === null) return null;
414
+ const anchor = genomes.find((g)=>g && g.is_anchor && String(g.anchor_taxon_id) === group);
415
+ return anchor ? anchor.taxon_id : null;
416
+ });
373
417
  const $9d9aeaf9299e61a1$var$expressionStudies = (0, $gXNCa$reduxbundler.createAsyncResourceBundle)({
374
418
  name: 'expressionStudies',
375
419
  actionBaseType: 'EXPRESSION_STUDIES',
@@ -672,50 +716,6 @@ const $9d9aeaf9299e61a1$var$grameneTaxDist = {
672
716
  return null;
673
717
  })
674
718
  };
675
- // this query will give the sorghum orthologs of AT1G01260
676
- // want to provide these on non-sorghum genes if available
677
- // http://data.gramene.org/search?q=homology__all_orthologs:AT1G01260&fq=taxon_id:4558&fl=id
678
- const $9d9aeaf9299e61a1$var$grameneOrthologs = {
679
- name: 'grameneOrthologs',
680
- getReducer: ()=>{
681
- const initialState = {};
682
- return (state = initialState, { type: type, payload: payload })=>{
683
- let newState;
684
- switch(type){
685
- case 'GRAMENE_ORTHOLOGS_REQUESTED':
686
- if (!state.hasOwnProperty(payload)) {
687
- newState = Object.assign({}, state);
688
- newState[payload] = [];
689
- return newState;
690
- }
691
- break;
692
- case 'GRAMENE_ORTHOLOGS_RECEIVED':
693
- return Object.assign({}, state, payload);
694
- }
695
- return state;
696
- };
697
- },
698
- doRequestOrthologs: (geneId)=>({ dispatch: dispatch, store: store })=>{
699
- const orthologs = store.selectGrameneOrthologs();
700
- if (!orthologs.hasOwnProperty(geneId)) {
701
- dispatch({
702
- type: 'GRAMENE_ORTHOLOGS_REQUESTED',
703
- payload: geneId
704
- });
705
- const API = store.selectGrameneAPI();
706
- const taxonId = store.selectTargetTaxonId();
707
- fetch(`${API}/search?q=homology__all_orthologs:${geneId}&fq=taxon_id:${taxonId}&fl=id`).then((res)=>res.json()).then((res)=>{
708
- let newOrthologs = {};
709
- newOrthologs[geneId] = res.response.docs.map((d)=>d.id);
710
- dispatch({
711
- type: 'GRAMENE_ORTHOLOGS_RECEIVED',
712
- payload: newOrthologs
713
- });
714
- });
715
- }
716
- },
717
- selectGrameneOrthologs: (state)=>state.grameneOrthologs
718
- };
719
719
  const $9d9aeaf9299e61a1$var$grameneParalogs = {
720
720
  name: 'grameneParalogs',
721
721
  getReducer: ()=>{
@@ -819,7 +819,6 @@ $9d9aeaf9299e61a1$export$2e2bcd8739ae039 = [
819
819
  $9d9aeaf9299e61a1$var$grameneMaps,
820
820
  $9d9aeaf9299e61a1$var$grameneTaxonomy,
821
821
  $9d9aeaf9299e61a1$var$grameneTaxDist,
822
- $9d9aeaf9299e61a1$var$grameneOrthologs,
823
822
  $9d9aeaf9299e61a1$var$grameneParalogs,
824
823
  $9d9aeaf9299e61a1$var$grameneGermplasm,
825
824
  $9d9aeaf9299e61a1$var$expressionSamples,
@@ -11484,10 +11483,6 @@ class $6c5c4f90059875bf$var$Gene extends (0, ($parcel$interopDefault($gXNCa$reac
11484
11483
  const panSite = this.props.config.panSite;
11485
11484
  const searchResult = this.props.searchResult;
11486
11485
  const taxName = this.props.taxName;
11487
- // let orthologs='';
11488
- // if (this.props.orthologs && this.props.orthologs.hasOwnProperty(searchResult.id)) {
11489
- // orthologs = this.props.orthologs[searchResult.id].join(', ');
11490
- // }
11491
11486
  const numWordsInDescription = searchResult.description.split(' ').length;
11492
11487
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11493
11488
  className: "result-gene",
@@ -11646,8 +11641,6 @@ const $6c5c4f90059875bf$var$GeneList = (props)=>{
11646
11641
  taxName: props.grameneTaxonomy[g.taxon_id].name,
11647
11642
  geneDocs: props.grameneGenes,
11648
11643
  requestGene: props.doRequestGrameneGene,
11649
- requestOrthologs: props.doRequestOrthologs,
11650
- orthologs: props.grameneOrthologs,
11651
11644
  taxLut: props.grameneTaxonomy,
11652
11645
  expandedDetail: props.grameneSearch.response.numFound === 1 && g.can_show.homology ? 'homology' : null
11653
11646
  }, idx)),
@@ -11659,7 +11652,7 @@ const $6c5c4f90059875bf$var$GeneList = (props)=>{
11659
11652
  }
11660
11653
  return null;
11661
11654
  };
11662
- var $6c5c4f90059875bf$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGrameneSearch', 'selectGrameneTaxonomy', 'selectGrameneGenes', 'selectGrameneOrthologs', 'selectGrameneSearchOffset', 'selectGrameneSearchRows', 'doRequestGrameneGene', 'doRequestOrthologs', 'doRequestResultsPage', $6c5c4f90059875bf$var$GeneList);
11655
+ var $6c5c4f90059875bf$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectConfiguration', 'selectGrameneSearch', 'selectGrameneTaxonomy', 'selectGrameneGenes', 'selectGrameneSearchOffset', 'selectGrameneSearchRows', 'doRequestGrameneGene', 'doRequestResultsPage', $6c5c4f90059875bf$var$GeneList);
11663
11656
 
11664
11657
 
11665
11658
 
@@ -13176,7 +13169,540 @@ var $261baeb81c4d4d8a$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
13176
13169
 
13177
13170
 
13178
13171
 
13179
- const $0f50f369018a42ef$var$MAX_GENE_IDS = 1000; // Define the maximum number of gene IDs allowed
13172
+
13173
+
13174
+
13175
+
13176
+
13177
+ // Conflict rows rendered before the "show N more" cap, mirroring the Refine
13178
+ // sidebar's INITIAL_VISIBLE. A large paste can produce hundreds of conflicts.
13179
+ const $3b525b9a0ec89bcb$var$INITIAL_VISIBLE = 25;
13180
+ // Candidates shown per row before collapsing. Nothing guarantees 15 is the ceiling.
13181
+ const $3b525b9a0ec89bcb$var$MAX_CANDIDATES = 50;
13182
+ // Genome bulk buttons offered. More than this and the toolbar is worse than the rows.
13183
+ const $3b525b9a0ec89bcb$var$MAX_GENOME_ACTIONS = 6;
13184
+ // A collapsible bucket with a count, following facets/FacetCounts.js FacetGroup.
13185
+ // An empty bucket renders nothing rather than an empty heading.
13186
+ const $3b525b9a0ec89bcb$var$Bucket = ({ title: title, count: count, tone: tone, defaultOpen: defaultOpen, actions: actions, children: children })=>{
13187
+ const [open, setOpen] = (0, $gXNCa$react.useState)(!!defaultOpen);
13188
+ if (!count) return null;
13189
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13190
+ className: `glr-bucket glr-bucket-${tone}`,
13191
+ children: [
13192
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13193
+ className: "glr-bucket-header",
13194
+ onClick: ()=>setOpen(!open),
13195
+ children: [
13196
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13197
+ className: "glr-caret",
13198
+ children: open ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {})
13199
+ }),
13200
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13201
+ className: "glr-bucket-title",
13202
+ children: title
13203
+ }),
13204
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13205
+ className: "glr-bucket-count",
13206
+ children: count.toLocaleString()
13207
+ }),
13208
+ actions && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13209
+ className: "glr-bucket-actions",
13210
+ children: actions
13211
+ })
13212
+ ]
13213
+ }),
13214
+ open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13215
+ className: "glr-bucket-body",
13216
+ children: children
13217
+ })
13218
+ ]
13219
+ });
13220
+ };
13221
+ /** "sorghum_bicolort2tcas" -> "Sorghum bicolort2tcas" */ function $3b525b9a0ec89bcb$var$speciesLabel(systemName) {
13222
+ if (!systemName) return null;
13223
+ const s = systemName.replace(/_/g, ' ');
13224
+ return s.charAt(0).toUpperCase() + s.slice(1);
13225
+ }
13226
+ /**
13227
+ * Pure resolver for a validation response.
13228
+ *
13229
+ * `choices` is keyed by INDEX into `ambiguous`, not by the input string: a pasted
13230
+ * token can be anything, and `obj['__proto__'] = [...]` on a plain object sets the
13231
+ * prototype instead of a key.
13232
+ *
13233
+ * An index with NO entry keeps *all* of that input's matches — the "nothing is
13234
+ * silently dropped" default. An index mapped to `[]` means the user explicitly
13235
+ * dropped the row, which is why every read tests `undefined` rather than falsiness.
13236
+ */ const $3b525b9a0ec89bcb$var$GeneListResolver = ({ validation: validation, choices: choices, meta: meta, onChange: onChange })=>{
13237
+ const [query, setQuery] = (0, $gXNCa$react.useState)('');
13238
+ const [showAll, setShowAll] = (0, $gXNCa$react.useState)(false);
13239
+ const [expanded, setExpanded] = (0, $gXNCa$react.useState)({});
13240
+ const { resolved: resolved, ambiguous: ambiguous, unknown: unknown } = validation;
13241
+ const keptFor = (i, a)=>choices[i] === undefined ? a.matches : choices[i];
13242
+ const setKept = (i, a, next)=>{
13243
+ const c = {
13244
+ ...choices
13245
+ };
13246
+ // Drop back to the default when everything is kept, so "untouched" and
13247
+ // "explicitly kept all" stay indistinguishable and the state stays small.
13248
+ if (next.length === a.matches.length) delete c[i];
13249
+ else c[i] = next;
13250
+ onChange(c);
13251
+ };
13252
+ const toggle = (i, a, id)=>{
13253
+ const cur = keptFor(i, a);
13254
+ setKept(i, a, cur.indexOf(id) === -1 ? [
13255
+ ...cur,
13256
+ id
13257
+ ] : cur.filter((x)=>x !== id));
13258
+ };
13259
+ /**
13260
+ * Which genomes would actually narrow something.
13261
+ *
13262
+ * A plain "prefer sorghum" button is useless on this index: 94 of its ~100
13263
+ * genomes are sorghum accessions, so a conflict like Sobic.001G000200 — whose
13264
+ * candidates are sorghum_bicolor and sorghum_bicolort2tcas — is entirely
13265
+ * sorghum on both sides. The useful axis is the individual genome, and only
13266
+ * those that would change a row are worth offering.
13267
+ */ const genomeActions = (0, $gXNCa$react.useMemo)(()=>{
13268
+ const tally = new Map();
13269
+ ambiguous.forEach((a)=>{
13270
+ const names = new Set();
13271
+ a.matches.forEach((id)=>{
13272
+ const d = meta[id];
13273
+ if (d && d.system_name) names.add(d.system_name);
13274
+ });
13275
+ names.forEach((sn)=>{
13276
+ const kept = a.matches.filter((id)=>meta[id] && meta[id].system_name === sn);
13277
+ // Only counts if it narrows: keeping every candidate is a no-op.
13278
+ if (kept.length && kept.length < a.matches.length) tally.set(sn, (tally.get(sn) || 0) + 1);
13279
+ });
13280
+ });
13281
+ return [
13282
+ ...tally.entries()
13283
+ ].sort((x, y)=>y[1] - x[1]).slice(0, $3b525b9a0ec89bcb$var$MAX_GENOME_ACTIONS);
13284
+ }, [
13285
+ ambiguous,
13286
+ meta
13287
+ ]);
13288
+ const applyGenome = (systemName)=>(e)=>{
13289
+ e.stopPropagation();
13290
+ const c = {
13291
+ ...choices
13292
+ };
13293
+ ambiguous.forEach((a, i)=>{
13294
+ const kept = a.matches.filter((id)=>meta[id] && meta[id].system_name === systemName);
13295
+ // Rows with no candidate in this genome are left exactly as they are,
13296
+ // rather than emptied.
13297
+ if (kept.length && kept.length < a.matches.length) c[i] = kept;
13298
+ });
13299
+ onChange(c);
13300
+ };
13301
+ const bulk = (mode)=>(e)=>{
13302
+ // These buttons sit inside the clickable header; don't collapse the section.
13303
+ e.stopPropagation();
13304
+ if (mode === 'all') return onChange({});
13305
+ const c = {};
13306
+ ambiguous.forEach((a, i)=>{
13307
+ if (mode === 'first' && a.matches.length > 1) c[i] = [
13308
+ a.matches[0]
13309
+ ];
13310
+ if (mode === 'none') c[i] = [];
13311
+ });
13312
+ onChange(c);
13313
+ };
13314
+ const filtered = (0, $gXNCa$react.useMemo)(()=>{
13315
+ const q = query.trim().toLowerCase();
13316
+ const withIndex = ambiguous.map((a, i)=>({
13317
+ a: a,
13318
+ i: i
13319
+ }));
13320
+ if (!q) return withIndex;
13321
+ return withIndex.filter(({ a: a })=>a.input.toLowerCase().indexOf(q) !== -1 || a.matches.some((id)=>id.toLowerCase().indexOf(q) !== -1));
13322
+ }, [
13323
+ ambiguous,
13324
+ query
13325
+ ]);
13326
+ const shown = showAll ? filtered : filtered.slice(0, $3b525b9a0ec89bcb$var$INITIAL_VISIBLE);
13327
+ const candidate = (i, a, id)=>{
13328
+ const doc = meta[id];
13329
+ const kept = keptFor(i, a).indexOf(id) !== -1;
13330
+ const species = $3b525b9a0ec89bcb$var$speciesLabel(doc && doc.system_name);
13331
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("label", {
13332
+ className: `glr-candidate${kept ? ' glr-candidate-kept' : ''}`,
13333
+ children: [
13334
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
13335
+ type: "checkbox",
13336
+ checked: kept,
13337
+ onChange: ()=>toggle(i, a, id)
13338
+ }),
13339
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13340
+ className: "glr-candidate-id",
13341
+ children: id
13342
+ }),
13343
+ species ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13344
+ className: "glr-candidate-species",
13345
+ children: species
13346
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13347
+ className: "glr-candidate-species glr-muted",
13348
+ children: "\u2026"
13349
+ }),
13350
+ doc && doc.description && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13351
+ className: "glr-candidate-desc",
13352
+ title: doc.description,
13353
+ children: doc.description
13354
+ }),
13355
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
13356
+ type: "button",
13357
+ className: "glr-only",
13358
+ title: "Keep only this one",
13359
+ onClick: ()=>setKept(i, a, [
13360
+ id
13361
+ ]),
13362
+ children: "only"
13363
+ })
13364
+ ]
13365
+ }, id);
13366
+ };
13367
+ const row = ({ a: a, i: i })=>{
13368
+ const kept = keptFor(i, a);
13369
+ // Long candidate lists start collapsed so a 15-match row doesn't bury the rest.
13370
+ const isOpen = expanded[i] !== undefined ? expanded[i] : a.matches.length <= 3;
13371
+ const visible = isOpen ? a.matches.slice(0, $3b525b9a0ec89bcb$var$MAX_CANDIDATES) : [];
13372
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13373
+ className: "glr-row",
13374
+ children: [
13375
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13376
+ className: "glr-input",
13377
+ children: [
13378
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
13379
+ type: "button",
13380
+ className: "glr-row-toggle",
13381
+ onClick: ()=>setExpanded((s)=>({
13382
+ ...s,
13383
+ [i]: !isOpen
13384
+ })),
13385
+ children: isOpen ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {})
13386
+ }),
13387
+ a.input,
13388
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Badge), {
13389
+ bg: kept.length === 0 ? 'danger' : 'secondary',
13390
+ className: "glr-input-count",
13391
+ children: [
13392
+ kept.length,
13393
+ "/",
13394
+ a.matches.length
13395
+ ]
13396
+ })
13397
+ ]
13398
+ }),
13399
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13400
+ className: "glr-candidates",
13401
+ children: isOpen ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13402
+ children: [
13403
+ visible.map((id)=>candidate(i, a, id)),
13404
+ a.matches.length > $3b525b9a0ec89bcb$var$MAX_CANDIDATES && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
13405
+ className: "glr-hint",
13406
+ children: [
13407
+ "\u2026 ",
13408
+ a.matches.length - $3b525b9a0ec89bcb$var$MAX_CANDIDATES,
13409
+ " more. Search to narrow."
13410
+ ]
13411
+ }),
13412
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
13413
+ className: "glr-row-bulk",
13414
+ children: [
13415
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
13416
+ type: "button",
13417
+ className: "glr-only",
13418
+ onClick: ()=>setKept(i, a, a.matches),
13419
+ children: "all"
13420
+ }),
13421
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("button", {
13422
+ type: "button",
13423
+ className: "glr-only",
13424
+ onClick: ()=>setKept(i, a, []),
13425
+ children: "none"
13426
+ })
13427
+ ]
13428
+ })
13429
+ ]
13430
+ }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
13431
+ className: "glr-collapsed",
13432
+ children: [
13433
+ a.matches.length,
13434
+ " candidates \u2014 ",
13435
+ kept.length === a.matches.length ? 'all kept' : `${kept.length} kept`
13436
+ ]
13437
+ })
13438
+ })
13439
+ ]
13440
+ }, i);
13441
+ };
13442
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13443
+ className: "glr",
13444
+ children: [
13445
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)($3b525b9a0ec89bcb$var$Bucket, {
13446
+ title: "Conflicts \u2014 one identifier, several genes",
13447
+ count: ambiguous.length,
13448
+ tone: "warn",
13449
+ defaultOpen: true,
13450
+ actions: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13451
+ children: [
13452
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13453
+ size: "sm",
13454
+ variant: "outline-secondary",
13455
+ onClick: bulk('first'),
13456
+ children: "Keep first"
13457
+ }),
13458
+ ' ',
13459
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13460
+ size: "sm",
13461
+ variant: "outline-secondary",
13462
+ onClick: bulk('none'),
13463
+ children: "Drop all"
13464
+ }),
13465
+ ' ',
13466
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13467
+ size: "sm",
13468
+ variant: "outline-secondary",
13469
+ onClick: bulk('all'),
13470
+ children: "Reset"
13471
+ })
13472
+ ]
13473
+ }),
13474
+ children: [
13475
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("p", {
13476
+ className: "glr-hint",
13477
+ children: [
13478
+ "These identifiers match more than one gene. ",
13479
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
13480
+ children: "All matches are kept unless you narrow them"
13481
+ }),
13482
+ " \u2014 untick what you don't want, or use",
13483
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
13484
+ children: " only"
13485
+ }),
13486
+ " to keep a single gene."
13487
+ ]
13488
+ }),
13489
+ genomeActions.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13490
+ className: "glr-genomes",
13491
+ children: [
13492
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13493
+ className: "glr-hint glr-genomes-label",
13494
+ children: "Keep only matches from:"
13495
+ }),
13496
+ genomeActions.map(([sn, n])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Button), {
13497
+ size: "sm",
13498
+ variant: "outline-primary",
13499
+ className: "glr-genome-btn",
13500
+ onClick: applyGenome(sn),
13501
+ children: [
13502
+ $3b525b9a0ec89bcb$var$speciesLabel(sn),
13503
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Badge), {
13504
+ bg: "light",
13505
+ text: "dark",
13506
+ className: "glr-genome-count",
13507
+ children: [
13508
+ "narrows ",
13509
+ n.toLocaleString()
13510
+ ]
13511
+ })
13512
+ ]
13513
+ }, sn))
13514
+ ]
13515
+ }),
13516
+ ambiguous.length > $3b525b9a0ec89bcb$var$INITIAL_VISIBLE && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
13517
+ type: "search",
13518
+ size: "sm",
13519
+ className: "glr-search",
13520
+ placeholder: "Search conflicts\u2026",
13521
+ value: query,
13522
+ onChange: (e)=>{
13523
+ setQuery(e.target.value);
13524
+ setShowAll(false);
13525
+ }
13526
+ }),
13527
+ shown.map(row),
13528
+ filtered.length > shown.length && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("button", {
13529
+ type: "button",
13530
+ className: "glr-more",
13531
+ onClick: ()=>setShowAll(true),
13532
+ children: [
13533
+ "show ",
13534
+ (filtered.length - shown.length).toLocaleString(),
13535
+ " more"
13536
+ ]
13537
+ }),
13538
+ query && filtered.length === 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("p", {
13539
+ className: "glr-hint",
13540
+ children: [
13541
+ "No conflicts match \u201C",
13542
+ query,
13543
+ "\u201D."
13544
+ ]
13545
+ })
13546
+ ]
13547
+ }),
13548
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($3b525b9a0ec89bcb$var$Bucket, {
13549
+ title: "Resolved",
13550
+ count: resolved.length,
13551
+ tone: "ok",
13552
+ defaultOpen: false,
13553
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
13554
+ className: "glr-resolved",
13555
+ children: resolved.map((r, i)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13556
+ className: "glr-resolved-row",
13557
+ children: [
13558
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13559
+ className: "glr-resolved-input",
13560
+ children: r.input
13561
+ }),
13562
+ r.input !== r.id && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13563
+ children: [
13564
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13565
+ className: "glr-arrow",
13566
+ children: "\u2192"
13567
+ }),
13568
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
13569
+ className: "glr-resolved-id",
13570
+ children: r.id
13571
+ })
13572
+ ]
13573
+ })
13574
+ ]
13575
+ }, i))
13576
+ })
13577
+ }),
13578
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)($3b525b9a0ec89bcb$var$Bucket, {
13579
+ title: "Not recognised",
13580
+ count: unknown.length,
13581
+ tone: "bad",
13582
+ defaultOpen: false,
13583
+ actions: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13584
+ size: "sm",
13585
+ variant: "outline-secondary",
13586
+ onClick: (e)=>{
13587
+ e.stopPropagation();
13588
+ navigator.clipboard && navigator.clipboard.writeText(unknown.join('\n'));
13589
+ },
13590
+ children: "Copy"
13591
+ }),
13592
+ children: [
13593
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("p", {
13594
+ className: "glr-hint",
13595
+ children: "No gene matches these. Matching is case-sensitive for stable IDs, so a lowercased ID may not be found even when the gene exists."
13596
+ }),
13597
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
13598
+ className: "glr-unknown",
13599
+ children: unknown.join('\n')
13600
+ })
13601
+ ]
13602
+ })
13603
+ ]
13604
+ });
13605
+ };
13606
+ var $3b525b9a0ec89bcb$export$2e2bcd8739ae039 = $3b525b9a0ec89bcb$var$GeneListResolver;
13607
+
13608
+
13609
+ // Client for the gene-lists API ({api}/gene_lists*).
13610
+ //
13611
+ // Mirrors the shape of bundles/savedViews.js: Firebase Bearer token, one
13612
+ // status -> message mapper so every call site reports failures the same way, and
13613
+ // JSON request bodies. Deliberately a plain module rather than a redux bundle —
13614
+ // the feature is self-contained in UserGeneLists and shares no state.
13615
+ // Server-side limits, mirrored so we can say something useful instead of
13616
+ // surfacing a bare 400.
13617
+ const $4abf453405c18681$export$7fadd81f3363fc57 = 6000;
13618
+ const $4abf453405c18681$export$7f2835aa6d834dde = 255;
13619
+ // Candidate hydration passes ids through the URL. ~500 ids is a 414 (the URL
13620
+ // crosses ~8.5KB), so batch well under that.
13621
+ const $4abf453405c18681$var$HYDRATE_BATCH = 150;
13622
+ function $4abf453405c18681$export$cbb0be6dabd4295(action, status) {
13623
+ if (status === 401) return new Error(`${action} failed \u{2014} please sign in again.`);
13624
+ if (status === 404) return new Error(`${action} failed \u{2014} that list no longer exists.`);
13625
+ if (status === 405) return new Error('Gene lists are not available on this server yet.');
13626
+ if (status === 502 || status === 503) return new Error(`${action} failed \u{2014} the gene index is unreachable. Try again shortly.`);
13627
+ return new Error(`${action} failed (${status})`);
13628
+ }
13629
+ // The API returns {message} on most 4xx; prefer it, since it names the offending
13630
+ // field (e.g. which limit was exceeded).
13631
+ async function $4abf453405c18681$var$readError(res, action) {
13632
+ try {
13633
+ const body = await res.json();
13634
+ if (body && body.message) return new Error(body.message);
13635
+ } catch (e) {
13636
+ // not JSON — fall through to the status mapper
13637
+ }
13638
+ return $4abf453405c18681$export$cbb0be6dabd4295(action, res.status);
13639
+ }
13640
+ async function $4abf453405c18681$export$6c06d9a9d837c44e(api, ids) {
13641
+ const res = await fetch(`${api}/gene_lists/validate`, {
13642
+ method: 'POST',
13643
+ headers: {
13644
+ 'Content-Type': 'application/json'
13645
+ },
13646
+ body: JSON.stringify(ids)
13647
+ });
13648
+ if (!res.ok) throw await $4abf453405c18681$var$readError(res, 'Validation');
13649
+ const body = await res.json();
13650
+ // Be strict about the contract. A server still returning the old
13651
+ // {ids, missing, hash} shape must fail here and say so, rather than quietly
13652
+ // validating nothing — which is exactly how the previous client broke.
13653
+ if (!body || !Array.isArray(body.resolved)) throw new Error('Unexpected response from the validation service (is the server up to date?).');
13654
+ return {
13655
+ resolved: body.resolved || [],
13656
+ ambiguous: body.ambiguous || [],
13657
+ unknown: body.unknown || []
13658
+ };
13659
+ }
13660
+ async function $4abf453405c18681$export$41928ff990e251ce(api, ids) {
13661
+ const unique = [
13662
+ ...new Set(ids)
13663
+ ].filter(Boolean);
13664
+ const out = {};
13665
+ for(let i = 0; i < unique.length; i += $4abf453405c18681$var$HYDRATE_BATCH){
13666
+ const batch = unique.slice(i, i + $4abf453405c18681$var$HYDRATE_BATCH);
13667
+ // {!terms} takes its values literally, so no id needs escaping, and it sits in
13668
+ // `fq`, where Solr local params are reliable.
13669
+ const fq = encodeURIComponent(`{!terms f=id}${batch.join(',')}`);
13670
+ // `rows` is mandatory. Without it the endpoint answers with its default of 20
13671
+ // and the rest of the batch silently disappears.
13672
+ const url = `${api}/search?q=*:*&fq=${fq}&rows=${batch.length}` + `&fl=id,name,system_name,description`;
13673
+ try {
13674
+ const res = await fetch(url);
13675
+ if (!res.ok) continue;
13676
+ const body = await res.json();
13677
+ const docs = body && body.response && body.response.docs || [];
13678
+ docs.forEach((d)=>{
13679
+ if (d && d.id) out[d.id] = d;
13680
+ });
13681
+ } catch (e) {
13682
+ // leave this batch un-hydrated
13683
+ }
13684
+ }
13685
+ return out;
13686
+ }
13687
+ async function $4abf453405c18681$export$87f42b3480ee6c49(api, token, { label: label, site: site, isPublic: isPublic, ids: ids }) {
13688
+ const res = await fetch(`${api}/gene_lists`, {
13689
+ method: 'POST',
13690
+ headers: {
13691
+ 'Content-Type': 'application/json',
13692
+ Authorization: `Bearer ${token}`
13693
+ },
13694
+ body: JSON.stringify({
13695
+ label: label,
13696
+ site: site,
13697
+ isPublic: !!isPublic,
13698
+ ids: ids
13699
+ })
13700
+ });
13701
+ if (!res.ok) throw await $4abf453405c18681$var$readError(res, 'Save');
13702
+ return res.json();
13703
+ }
13704
+
13705
+
13180
13706
  const $0f50f369018a42ef$var$formatDate = (value)=>{
13181
13707
  if (!value) return '';
13182
13708
  const d = new Date(value);
@@ -13727,14 +14253,20 @@ const $0f50f369018a42ef$var$GeneListDisplayComponent = (props)=>{
13727
14253
  };
13728
14254
  const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13729
14255
  const [geneList, setGeneList] = (0, $gXNCa$react.useState)('');
13730
- const [listHash, setListHash] = (0, $gXNCa$react.useState)(null);
13731
- const [validationError, setValidationError] = (0, $gXNCa$react.useState)([]);
13732
14256
  const [listName, setListName] = (0, $gXNCa$react.useState)('');
13733
14257
  const [listIsPublic, setListIsPublic] = (0, $gXNCa$react.useState)(false);
13734
- const [validatedList, setValidatedList] = (0, $gXNCa$react.useState)([]);
14258
+ // The three-bucket validate response, the user's narrowing decisions, and the
14259
+ // hydrated gene docs used to label candidates.
14260
+ const [validation, setValidation] = (0, $gXNCa$react.useState)(null);
14261
+ const [choices, setChoices] = (0, $gXNCa$react.useState)({});
14262
+ const [meta, setMeta] = (0, $gXNCa$react.useState)({});
13735
14263
  const [errorMessage, setErrorMessage] = (0, $gXNCa$react.useState)('');
13736
- const [loading, setLoading] = (0, $gXNCa$react.useState)(false); // New loading state
13737
- const [user, setUser] = (0, $gXNCa$react.useState)({});
14264
+ const [notice, setNotice] = (0, $gXNCa$react.useState)('');
14265
+ const [loading, setLoading] = (0, $gXNCa$react.useState)(false);
14266
+ const [saving, setSaving] = (0, $gXNCa$react.useState)(false);
14267
+ // null, not {}: the initial value is read to decide whether Save is enabled, and
14268
+ // an empty object is truthy, which used to offer Save to a logged-out visitor.
14269
+ const [user, setUser] = (0, $gXNCa$react.useState)(null);
13738
14270
  const auth = props.auth;
13739
14271
  (0, $gXNCa$react.useEffect)(()=>{
13740
14272
  if (!auth) return;
@@ -13743,6 +14275,38 @@ const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13743
14275
  }, [
13744
14276
  auth
13745
14277
  ]);
14278
+ // The single source of truth for what gets saved. An ambiguous input the user
14279
+ // never touched contributes ALL of its matches, so nothing is silently dropped;
14280
+ // narrowing is a choice, not a requirement.
14281
+ // `choices[i] === undefined` means untouched -> keep all. `choices[i] === []`
14282
+ // means the user explicitly dropped the row, so this must NOT be `|| a.matches`:
14283
+ // an empty array is falsy, and that would silently re-add everything they just
14284
+ // removed.
14285
+ const keptFor = (i, a)=>choices[i] === undefined ? a.matches : choices[i];
14286
+ const idsToSave = (0, $gXNCa$react.useMemo)(()=>{
14287
+ if (!validation) return [];
14288
+ const out = validation.resolved.map((r)=>r.id);
14289
+ validation.ambiguous.forEach((a, i)=>out.push(...keptFor(i, a)));
14290
+ // Mandatory, not tidiness: the same gene reaches this list twice routinely —
14291
+ // two inputs can resolve to one id, and a resolved id can also appear among
14292
+ // another input's candidates. Duplicates would inflate n_genes and change the
14293
+ // server's content hash, breaking the save endpoint's idempotency.
14294
+ return [
14295
+ ...new Set(out)
14296
+ ];
14297
+ }, [
14298
+ validation,
14299
+ choices
14300
+ ]);
14301
+ // How much of the total came from conflicts, for the summary line — with
14302
+ // keep-all-by-default a single identifier can contribute 15 genes.
14303
+ const fromConflicts = (0, $gXNCa$react.useMemo)(()=>{
14304
+ if (!validation) return 0;
14305
+ return validation.ambiguous.reduce((n, a, i)=>n + keptFor(i, a).length, 0);
14306
+ }, [
14307
+ validation,
14308
+ choices
14309
+ ]);
13746
14310
  // Function to handle gene list input
13747
14311
  const handleGeneListChange = (event)=>{
13748
14312
  setGeneList(event.target.value);
@@ -13759,65 +14323,66 @@ const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13759
14323
  };
13760
14324
  // Function to submit gene list for validation
13761
14325
  const handleSubmit = async ()=>{
13762
- const geneArray = geneList.split('\n').filter(Boolean); // Convert the gene list into an array and filter out empty values
13763
- if (geneArray.length > $0f50f369018a42ef$var$MAX_GENE_IDS) {
13764
- setErrorMessage(`You have exceeded the maximum limit of ${$0f50f369018a42ef$var$MAX_GENE_IDS} gene IDs.`);
14326
+ // The server only drops blanks and duplicates; splitting and trimming is ours
14327
+ // to do. Accept commas, tabs and semicolons as well as newlines.
14328
+ const geneArray = [
14329
+ ...new Set(geneList.split(/[\s,;]+/).map((s)=>s.trim()).filter(Boolean))
14330
+ ];
14331
+ if (geneArray.length === 0) {
14332
+ setErrorMessage('Paste at least one gene identifier.');
14333
+ return;
14334
+ }
14335
+ if (geneArray.length > (0, $4abf453405c18681$export$7fadd81f3363fc57)) {
14336
+ setErrorMessage(`That is ${geneArray.length.toLocaleString()} identifiers; the limit is ${(0, $4abf453405c18681$export$7fadd81f3363fc57).toLocaleString()}.`);
13765
14337
  return;
13766
14338
  }
13767
- setErrorMessage(''); // Reset error message if validation passes
13768
- setLoading(true); // Set loading state to true to show progress
14339
+ setErrorMessage('');
14340
+ setNotice('');
14341
+ setLoading(true);
13769
14342
  try {
13770
- const response = await fetch(`${props.api}/gene_lists/validate`, {
13771
- method: 'POST',
13772
- headers: {
13773
- 'Content-Type': 'application/json'
13774
- },
13775
- body: JSON.stringify(geneArray)
13776
- });
13777
- const result = await response.json();
13778
- if (result.hash) {
13779
- setListHash(result.hash);
13780
- setValidatedList(result.ids);
13781
- setValidationError(result.missing);
13782
- } else // Handle errors from validation
13783
- alert('Error during validation.');
14343
+ const result = await (0, $4abf453405c18681$export$6c06d9a9d837c44e)(props.api, geneArray);
14344
+ setValidation(result);
14345
+ setChoices({});
14346
+ // Candidates are only choosable if the user can see what they are, so pull
14347
+ // species/description for every candidate id. Best-effort and non-blocking:
14348
+ // the resolver renders bare ids until this lands.
14349
+ const candidateIds = result.ambiguous.reduce((acc, a)=>acc.concat(a.matches), []);
14350
+ if (candidateIds.length) (0, $4abf453405c18681$export$41928ff990e251ce)(props.api, candidateIds).then(setMeta).catch(()=>{});
14351
+ else setMeta({});
13784
14352
  } catch (error) {
13785
- alert('There was an error with the validation service.', error);
14353
+ setValidation(null);
14354
+ setErrorMessage(error.message || 'There was an error with the validation service.');
13786
14355
  } finally{
13787
- setLoading(false); // Set loading to false when the request is complete
14356
+ setLoading(false);
13788
14357
  }
13789
14358
  };
13790
14359
  // Function to save the validated gene list
13791
14360
  const handleSaveList = async ()=>{
13792
- const queryParams = {
13793
- label: listName,
13794
- hash: listHash,
13795
- site: props.site,
13796
- n_genes: validatedList.length,
13797
- isPublic: listIsPublic
13798
- };
13799
- const queryString = new URLSearchParams(queryParams).toString();
13800
- const token = await user.getIdToken();
14361
+ setErrorMessage('');
14362
+ setNotice('');
14363
+ setSaving(true);
13801
14364
  try {
13802
- const response = await fetch(`${props.api}/gene_lists?${queryString}`, {
13803
- method: 'POST',
13804
- headers: {
13805
- "Content-Type": "application/json",
13806
- "Authorization": `Bearer ${token}`
13807
- }
14365
+ // Inside the try: getIdToken() rejects when the session has expired, and
14366
+ // that used to escape as an unhandled rejection with no UI feedback.
14367
+ const token = await user.getIdToken();
14368
+ const result = await (0, $4abf453405c18681$export$87f42b3480ee6c49)(props.api, token, {
14369
+ label: listName,
14370
+ site: props.site,
14371
+ isPublic: listIsPublic,
14372
+ ids: idsToSave
13808
14373
  });
13809
- if (!response.ok) throw new Error('Failed to save list');
13810
- const result = await response.json();
13811
- console.log(result);
13812
14374
  setGeneList('');
13813
- setListHash(null);
13814
- setValidatedList([]);
13815
- setValidationError([]);
14375
+ setValidation(null);
14376
+ setChoices({});
14377
+ setMeta({});
13816
14378
  setListName('');
13817
14379
  setListIsPublic(false);
14380
+ setNotice(`Saved \u{201C}${listName}\u{201D} \u{2014} ${(result.n_genes || idsToSave.length).toLocaleString()} genes.`);
13818
14381
  if (props.onListSaved) props.onListSaved();
13819
14382
  } catch (error) {
13820
- console.error("There was an problem with fetch", error);
14383
+ setErrorMessage(error.message || 'Failed to save the gene list.');
14384
+ } finally{
14385
+ setSaving(false);
13821
14386
  }
13822
14387
  };
13823
14388
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
@@ -13836,7 +14401,7 @@ const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13836
14401
  rows: 10,
13837
14402
  value: geneList,
13838
14403
  onChange: handleGeneListChange,
13839
- placeholder: `Paste or drop your gene list here (Maximum: ${$0f50f369018a42ef$var$MAX_GENE_IDS} IDs)`,
14404
+ placeholder: `Paste or drop your gene list here (Maximum: ${(0, $4abf453405c18681$export$7fadd81f3363fc57)} IDs)`,
13840
14405
  onDrop: handleDrop,
13841
14406
  onDragOver: handleDragOver
13842
14407
  })
@@ -13844,8 +14409,16 @@ const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13844
14409
  }),
13845
14410
  errorMessage && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Alert), {
13846
14411
  variant: "danger",
14412
+ dismissible: true,
14413
+ onClose: ()=>setErrorMessage(''),
13847
14414
  children: errorMessage
13848
14415
  }),
14416
+ notice && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Alert), {
14417
+ variant: "success",
14418
+ dismissible: true,
14419
+ onClose: ()=>setNotice(''),
14420
+ children: notice
14421
+ }),
13849
14422
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13850
14423
  variant: "primary",
13851
14424
  onClick: handleSubmit,
@@ -13869,53 +14442,55 @@ const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13869
14442
  })
13870
14443
  ]
13871
14444
  }),
13872
- !loading && listHash && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13873
- className: "validation-summary mt-3 p-3 border rounded bg-light",
14445
+ !loading && validation && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
13874
14446
  children: [
14447
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $3b525b9a0ec89bcb$export$2e2bcd8739ae039), {
14448
+ validation: validation,
14449
+ choices: choices,
14450
+ meta: meta,
14451
+ onChange: setChoices
14452
+ }),
13875
14453
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14454
+ className: "glr-summary",
13876
14455
  children: [
13877
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
13878
- className: "text-success",
13879
- children: [
13880
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
13881
- children: validatedList.length
13882
- }),
13883
- " valid"
13884
- ]
14456
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
14457
+ className: "glr-summary-total",
14458
+ children: idsToSave.length.toLocaleString()
13885
14459
  }),
13886
- validationError.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13887
- className: "text-danger mt-1",
14460
+ ' ',
14461
+ "gene",
14462
+ idsToSave.length === 1 ? '' : 's',
14463
+ " will be saved",
14464
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14465
+ className: "glr-summary-detail",
13888
14466
  children: [
13889
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
13890
- children: validationError.length
14467
+ validation.resolved.length.toLocaleString(),
14468
+ " identifier",
14469
+ validation.resolved.length === 1 ? '' : 's',
14470
+ " matched exactly one gene",
14471
+ validation.ambiguous.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
14472
+ children: [
14473
+ '; ',
14474
+ validation.ambiguous.length.toLocaleString(),
14475
+ " conflicting identifier",
14476
+ validation.ambiguous.length === 1 ? '' : 's',
14477
+ " contributing",
14478
+ ' ',
14479
+ fromConflicts.toLocaleString(),
14480
+ " candidate",
14481
+ fromConflicts === 1 ? '' : 's',
14482
+ Object.keys(choices).length > 0 && ` (${Object.keys(choices).length.toLocaleString()} narrowed)`
14483
+ ]
13891
14484
  }),
13892
- " not found"
14485
+ '. ',
14486
+ "Genes matched more than once are counted once."
13893
14487
  ]
13894
14488
  })
13895
14489
  ]
13896
- }),
13897
- validationError.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("details", {
13898
- className: "mt-2",
13899
- children: [
13900
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("summary", {
13901
- style: {
13902
- cursor: 'pointer'
13903
- },
13904
- children: "Show unrecognized IDs"
13905
- }),
13906
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("pre", {
13907
- className: "small mt-2 mb-0",
13908
- style: {
13909
- maxHeight: 150,
13910
- overflow: 'auto'
13911
- },
13912
- children: validationError.join('\n')
13913
- })
13914
- ]
13915
14490
  })
13916
14491
  ]
13917
14492
  }),
13918
- !loading && validatedList.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
14493
+ !loading && idsToSave.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
13919
14494
  className: "save-list mt-4",
13920
14495
  children: [
13921
14496
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Group, {
@@ -13942,7 +14517,8 @@ const $0f50f369018a42ef$var$GeneListComponent = (props)=>{
13942
14517
  user ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13943
14518
  variant: "primary",
13944
14519
  onClick: handleSaveList,
13945
- children: "Save Gene List"
14520
+ disabled: saving || !listName.trim(),
14521
+ children: saving ? "Saving\u2026" : 'Save Gene List'
13946
14522
  }) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
13947
14523
  variant: "secondary",
13948
14524
  disabled: true,