gramene-search 2.21.0 → 2.22.1

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
@@ -850,6 +850,42 @@ function $af4441dd29af05df$var$markSubtree(node, source, invert) {
850
850
  });
851
851
  }
852
852
  const $af4441dd29af05df$export$6b95404109a3e2ec = 'Genomic intervals';
853
+ function $af4441dd29af05df$export$634f7214da1b780a({ map: map, region: region, start: start, end: end }) {
854
+ return `(map:${map} AND region:${region}` + ` AND start:[* TO ${end}] AND end:[${start} TO *])`;
855
+ }
856
+ function $af4441dd29af05df$export$535c09441f37845d({ mapLabel: mapLabel, map: map, region: region, start: start, end: end }) {
857
+ return `${mapLabel || map} ${region}:${Number(start).toLocaleString()}-${Number(end).toLocaleString()}`;
858
+ }
859
+ // Note the field order is inverted relative to the interval: `start` carries the
860
+ // interval's END and `end` carries its START, because the pair is an overlap test.
861
+ const $af4441dd29af05df$var$INTERVAL_RE = /^\(map:(\S+) AND region:(\S+) AND start:\[\* TO (\d+)\] AND end:\[(\d+) TO \*\]\)$/;
862
+ function $af4441dd29af05df$export$67920d3e3cb3f136(fqValue) {
863
+ const m = $af4441dd29af05df$var$INTERVAL_RE.exec(String(fqValue || ''));
864
+ if (!m) return null;
865
+ return {
866
+ map: m[1],
867
+ region: m[2],
868
+ end: Number(m[3]),
869
+ start: Number(m[4])
870
+ };
871
+ }
872
+ /** `[lo TO hi]`, as produced by the expression brush and the Location tab. */ const $af4441dd29af05df$var$RANGE_RE = /^\[\s*(\S+)\s+TO\s+(\S+)\s*\]$/;
873
+ function $af4441dd29af05df$export$9ee0ceaa589fc567(fqValue) {
874
+ const m = $af4441dd29af05df$var$RANGE_RE.exec(String(fqValue || ''));
875
+ return m ? {
876
+ lo: m[1],
877
+ hi: m[2]
878
+ } : null;
879
+ }
880
+ function $af4441dd29af05df$export$cd9b2ffd001c6d65(lo, hi) {
881
+ return `[${lo} TO ${hi}]`;
882
+ }
883
+ function $af4441dd29af05df$export$a7f4fa59930e5088(node) {
884
+ if (!node || node.hasOwnProperty('children')) return null;
885
+ if ($af4441dd29af05df$export$67920d3e3cb3f136(node.fq_value)) return 'interval';
886
+ if ($af4441dd29af05df$export$9ee0ceaa589fc567(node.fq_value)) return 'range';
887
+ return 'value';
888
+ }
853
889
  const $af4441dd29af05df$export$da3188ffd2da2525 = {
854
890
  orthologs: {
855
891
  label: 'orthologs',
@@ -1061,8 +1097,19 @@ const $af4441dd29af05df$var$grameneFilters = {
1061
1097
  // start, so a gene that merely crosses the boundary still matches.
1062
1098
  const intervalNode = {
1063
1099
  fq_field: 'location',
1064
- fq_value: `(map:${map} AND region:${region}` + ` AND start:[* TO ${end}] AND end:[${start} TO *])`,
1065
- name: `${mapLabel} ${region}:${start.toLocaleString()}-${end.toLocaleString()}`,
1100
+ fq_value: $af4441dd29af05df$export$634f7214da1b780a({
1101
+ map: map,
1102
+ region: region,
1103
+ start: start,
1104
+ end: end
1105
+ }),
1106
+ name: $af4441dd29af05df$export$535c09441f37845d({
1107
+ mapLabel: mapLabel,
1108
+ map: map,
1109
+ region: region,
1110
+ start: start,
1111
+ end: end
1112
+ }),
1066
1113
  category: 'Genomic interval',
1067
1114
  negate: false,
1068
1115
  showMenu: false
@@ -1084,6 +1131,24 @@ const $af4441dd29af05df$var$grameneFilters = {
1084
1131
  $af4441dd29af05df$var$markSubtree(newState, newState, false);
1085
1132
  return newState;
1086
1133
  }
1134
+ case 'GRAMENE_FILTER_EDITED':
1135
+ {
1136
+ // Edits only rewrite a leaf's value and label. The node keeps its place,
1137
+ // its negate flag and any expansion, and the tree shape is untouched — so
1138
+ // unlike an insert there is nothing to reindex.
1139
+ newState = Object.assign({}, state, {
1140
+ status: 'search',
1141
+ showMarked: true,
1142
+ searchOffset: 0
1143
+ });
1144
+ const node = $af4441dd29af05df$var$findNodeWithLeftIdx(newState, payload.leftIdx);
1145
+ if (!node || node.hasOwnProperty('children')) break;
1146
+ node.fq_value = payload.fq_value;
1147
+ if (payload.name !== undefined) node.name = payload.name;
1148
+ node.showMenu = false;
1149
+ $af4441dd29af05df$var$markSubtree(newState, node, false);
1150
+ return newState;
1151
+ }
1087
1152
  case 'GRAMENE_FILTER_EXPANSION_SET':
1088
1153
  {
1089
1154
  // Setting or clearing an expansion is just a field on the node — the tree
@@ -1419,6 +1484,28 @@ const $af4441dd29af05df$var$grameneFilters = {
1419
1484
  ]
1420
1485
  });
1421
1486
  },
1487
+ // Replace one leaf's value in place, keeping its position in the tree.
1488
+ doEditGrameneFilter: (filter, fq_value, name)=>({ dispatch: dispatch })=>{
1489
+ dispatch({
1490
+ type: 'BATCH_ACTIONS',
1491
+ actions: [
1492
+ {
1493
+ type: 'GRAMENE_SEARCH_CLEARED'
1494
+ },
1495
+ {
1496
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1497
+ },
1498
+ {
1499
+ type: 'GRAMENE_FILTER_EDITED',
1500
+ payload: {
1501
+ leftIdx: filter.leftIdx,
1502
+ fq_value: fq_value,
1503
+ name: name
1504
+ }
1505
+ }
1506
+ ]
1507
+ });
1508
+ },
1422
1509
  doToggleGrameneFilterMenu: (node)=>({ dispatch: dispatch })=>{
1423
1510
  dispatch({
1424
1511
  type: 'GRAMENE_FILTER_MENU_TOGGLED',
@@ -1918,10 +2005,22 @@ var $24971af0a229e0e3$export$2e2bcd8739ae039 = $24971af0a229e0e3$var$grameneView
1918
2005
  // and stable enough to keep around across sessions, so we bulk-load it
1919
2006
  // once with `?rows=-1` and reuse it instead of issuing per-id requests.
1920
2007
  // Scoped to the subsite so per-site pathway corpora don't collide.
1921
- const $671312b287158a8a$var$pathwayCache = (0, $gXNCa$moneyclip.getConfiguredCache)({
1922
- version: 1,
1923
- name: `gramene_pathways_${process.env.SUBSITE || 'default'}`
1924
- });
2008
+ //
2009
+ // The corpus is release-specific: each pathway doc carries its hierarchy in
2010
+ // per-genome `lineage_<taxon_id>` fields, and taxon ids change between data
2011
+ // releases (sorghum_v10b's 4558001 became sorghum_v11's 4558006). Serving a
2012
+ // previous release's docs therefore yields no lineage at all for the current
2013
+ // genomes, which used to leave the pathways tab building diagram ids like
2014
+ // `R-SBI-undefined`. Versioning the cache by the API URL makes money-clip
2015
+ // drop and refetch the corpus whenever the site moves to a new release.
2016
+ let $671312b287158a8a$var$pathwayCache = null;
2017
+ const $671312b287158a8a$var$getPathwayCache = (api)=>{
2018
+ if (!$671312b287158a8a$var$pathwayCache) $671312b287158a8a$var$pathwayCache = (0, $gXNCa$moneyclip.getConfiguredCache)({
2019
+ version: `1:${api}`,
2020
+ name: `gramene_pathways_${process.env.SUBSITE || 'default'}`
2021
+ });
2022
+ return $671312b287158a8a$var$pathwayCache;
2023
+ };
1925
2024
  let $671312b287158a8a$var$pathwaysBulkPromise = null;
1926
2025
  const $671312b287158a8a$var$grameneDocs = {
1927
2026
  name: 'grameneDocs',
@@ -2134,7 +2233,9 @@ const $671312b287158a8a$var$grameneDocs = {
2134
2233
  // subsequent calls.
2135
2234
  doRequestGramenePathways: (_ids)=>({ dispatch: dispatch, store: store })=>{
2136
2235
  if ($671312b287158a8a$var$pathwaysBulkPromise) return $671312b287158a8a$var$pathwaysBulkPromise;
2137
- $671312b287158a8a$var$pathwaysBulkPromise = $671312b287158a8a$var$pathwayCache.get('all').then((cached)=>{
2236
+ const api = store.selectGrameneAPI();
2237
+ const cache = $671312b287158a8a$var$getPathwayCache(api);
2238
+ $671312b287158a8a$var$pathwaysBulkPromise = cache.get('all').then((cached)=>{
2138
2239
  if (cached) {
2139
2240
  dispatch({
2140
2241
  type: 'GRAMENE_PATHWAYS_RECEIVED',
@@ -2142,7 +2243,7 @@ const $671312b287158a8a$var$grameneDocs = {
2142
2243
  });
2143
2244
  return;
2144
2245
  }
2145
- return fetch(`${store.selectGrameneAPI()}/pathways?rows=-1`).then((res)=>res.json()).then((res)=>{
2246
+ return fetch(`${api}/pathways?rows=-1`).then((res)=>res.json()).then((res)=>{
2146
2247
  const pathways = {};
2147
2248
  (res || []).forEach((p)=>{
2148
2249
  if (p && p._id != null) pathways[p._id] = p;
@@ -2151,7 +2252,7 @@ const $671312b287158a8a$var$grameneDocs = {
2151
2252
  type: 'GRAMENE_PATHWAYS_RECEIVED',
2152
2253
  payload: pathways
2153
2254
  });
2154
- $671312b287158a8a$var$pathwayCache.set('all', pathways).catch((e)=>console.warn('Failed to cache pathways', e));
2255
+ cache.set('all', pathways).catch((e)=>console.warn('Failed to cache pathways', e));
2155
2256
  });
2156
2257
  }).catch((err)=>{
2157
2258
  console.error('Failed to load pathways', err);
@@ -9805,6 +9906,7 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
9805
9906
  });
9806
9907
  }
9807
9908
  stableId(dbId) {
9909
+ if (dbId === undefined || dbId === null) return null;
9808
9910
  let prefix = this.taxonomy.indices.id[this.gene.taxon_id].model.reactomePrefix || 'OSA';
9809
9911
  return `R-${prefix}-${dbId}`;
9810
9912
  }
@@ -9818,7 +9920,7 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
9818
9920
  // If pathway docs are already cached, build the hierarchy now so the
9819
9921
  // initial render selects a default node (componentDidUpdate would
9820
9922
  // otherwise never run if no redux change follows the mount).
9821
- if (!this.state.hierarchy && this.pathwayIds) {
9923
+ if (!this.state.hierarchy && !this.state.hierarchyEmpty && this.pathwayIds) {
9822
9924
  const haveDocs = this.pathwayIds.filter((id)=>this.props.gramenePathways.hasOwnProperty(id));
9823
9925
  if (haveDocs.length === this.pathwayIds.length) {
9824
9926
  const docs = this.pathwayIds.map((id)=>this.props.gramenePathways[id]);
@@ -9838,7 +9940,7 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
9838
9940
  }
9839
9941
  this.loadNodes(path);
9840
9942
  }
9841
- if (!this.state.hierarchy && this.pathwayIds) {
9943
+ if (!this.state.hierarchy && !this.state.hierarchyEmpty && this.pathwayIds) {
9842
9944
  const haveDocs = this.pathwayIds.filter((id)=>this.props.gramenePathways.hasOwnProperty(id));
9843
9945
  if (haveDocs.length === this.pathwayIds.length) {
9844
9946
  const docs = this.pathwayIds.map((id)=>this.props.gramenePathways[id]);
@@ -9902,10 +10004,23 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
9902
10004
  const nested = new (0, ($parcel$interopDefault($gXNCa$flattonested)))({
9903
10005
  children: 'nodes'
9904
10006
  }).convert(nodes);
10007
+ // An empty node list, or lineages whose top ancestor isn't itself one of
10008
+ // the gene's pathways, make flat-to-nested return a root with no id. Walking
10009
+ // that root would ask Plant Reactome for `R-<prefix>-undefined`, so treat it
10010
+ // as "nothing to show" instead.
10011
+ if (nested.id === undefined && !nested.nodes) {
10012
+ this.setState({
10013
+ hierarchy: undefined,
10014
+ hierarchyEmpty: true,
10015
+ selectedNode: undefined
10016
+ });
10017
+ return;
10018
+ }
9905
10019
  this.setState({
9906
10020
  hierarchy: [
9907
10021
  nested
9908
10022
  ],
10023
+ hierarchyEmpty: false,
9909
10024
  selectedNode: undefined
9910
10025
  });
9911
10026
  }
@@ -9943,7 +10058,7 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
9943
10058
  nodeRef = nodeRef.nodes[0];
9944
10059
  lineage.unshift(nodeRef);
9945
10060
  }
9946
- if (lineage[0].id !== 2894885) {
10061
+ if (lineage[0].id !== undefined && lineage[0].id !== 2894885) {
9947
10062
  let pathway = this.stableId(lineage[0].id);
9948
10063
  let reaction = undefined;
9949
10064
  if (lineage[0].type === "Reaction") {
@@ -9985,6 +10100,14 @@ class $54c74a4689d5a778$var$Pathways extends (0, ($parcel$interopDefault($gXNCa$
9985
10100
  initialOpenNodes: allPaths
9986
10101
  });
9987
10102
  }
10103
+ if (this.state.hierarchyEmpty) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
10104
+ className: "pathways-tree-empty",
10105
+ children: [
10106
+ "No pathway hierarchy is available for ",
10107
+ this.gene.system_name || 'this genome',
10108
+ "."
10109
+ ]
10110
+ });
9988
10111
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Spinner), {});
9989
10112
  }
9990
10113
  updateQuery() {
@@ -20883,18 +21006,26 @@ function $909272efb41be16f$export$94955a075dcb14b2(genome) {
20883
21006
  length: lengths[i]
20884
21007
  })).filter((r)=>!$909272efb41be16f$var$UNPLACED.test(String(r.name)));
20885
21008
  }
20886
- const $909272efb41be16f$var$GenomicIntervalForm = ({ grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, onAdd: onAdd })=>{
20887
- const [open, setOpen] = (0, $gXNCa$react.useState)(false);
21009
+ const $909272efb41be16f$export$4fa3dba3103266ea = ({ grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, initial: initial, submitLabel: submitLabel, onSubmit: onSubmit, onCancel: onCancel })=>{
20888
21010
  const [taxonId, setTaxonId] = (0, $gXNCa$react.useState)('');
20889
- const [region, setRegion] = (0, $gXNCa$react.useState)('');
20890
- const [start, setStart] = (0, $gXNCa$react.useState)('');
20891
- const [end, setEnd] = (0, $gXNCa$react.useState)('');
21011
+ const [region, setRegion] = (0, $gXNCa$react.useState)(initial ? String(initial.region) : '');
21012
+ const [start, setStart] = (0, $gXNCa$react.useState)(initial ? String(initial.start) : '');
21013
+ const [end, setEnd] = (0, $gXNCa$react.useState)(initial ? String(initial.end) : '');
20892
21014
  const { reference: reference, anchors: anchors, rest: rest } = (0, $gXNCa$react.useMemo)(()=>$909272efb41be16f$export$ac97d17982e0cb21(grameneMaps, targetTaxonId), [
20893
21015
  grameneMaps,
20894
21016
  targetTaxonId
20895
21017
  ]);
20896
- // Default to the site's reference genome once maps have loaded.
20897
- const effectiveTaxon = taxonId || reference[0] && String(reference[0].taxon_id) || '';
21018
+ // An edit starts on the filter's own genome; a new interval starts on the
21019
+ // site's reference. `initial.map` is an assembly accession, so find its genome.
21020
+ const initialTaxon = (0, $gXNCa$react.useMemo)(()=>{
21021
+ if (!initial) return '';
21022
+ const hit = Object.values(grameneMaps || {}).find((m)=>m && m._id === initial.map);
21023
+ return hit ? String(hit.taxon_id) : '';
21024
+ }, [
21025
+ initial,
21026
+ grameneMaps
21027
+ ]);
21028
+ const effectiveTaxon = taxonId || initialTaxon || reference[0] && String(reference[0].taxon_id) || '';
20898
21029
  const genome = effectiveTaxon ? (grameneMaps || {})[effectiveTaxon] : null;
20899
21030
  const chromosomes = (0, $gXNCa$react.useMemo)(()=>$909272efb41be16f$export$94955a075dcb14b2(genome), [
20900
21031
  genome
@@ -20906,8 +21037,7 @@ const $909272efb41be16f$var$GenomicIntervalForm = ({ grameneMaps: grameneMaps, t
20906
21037
  // Validated against the assembly's own region lengths, so the message can name
20907
21038
  // the actual limit rather than just refusing.
20908
21039
  const error = (()=>{
20909
- if (!genome) return null;
20910
- if (s === null || e === null) return null;
21040
+ if (!genome || s === null || e === null) return null;
20911
21041
  if (!Number.isFinite(s) || !Number.isFinite(e)) return 'Positions must be numbers.';
20912
21042
  if (s < 1 || e < 1) return 'Positions start at 1.';
20913
21043
  if (s > e) return 'Start must not be greater than end.';
@@ -20917,112 +21047,134 @@ const $909272efb41be16f$var$GenomicIntervalForm = ({ grameneMaps: grameneMaps, t
20917
21047
  const ready = !!genome && !!effectiveRegion && s !== null && e !== null && !error;
20918
21048
  const submit = ()=>{
20919
21049
  if (!ready) return;
20920
- onAdd({
21050
+ onSubmit({
20921
21051
  map: genome._id,
20922
21052
  mapLabel: genome.display_name || genome._id,
20923
21053
  region: effectiveRegion,
20924
21054
  start: s,
20925
21055
  end: e
20926
21056
  });
20927
- setStart('');
20928
- setEnd('');
21057
+ if (!initial) {
21058
+ setStart('');
21059
+ setEnd('');
21060
+ }
20929
21061
  };
20930
21062
  const genomeOption = (m)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("option", {
20931
21063
  value: m.taxon_id,
20932
21064
  children: m.display_name || m.system_name
20933
21065
  }, m.taxon_id);
20934
21066
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20935
- className: "gramene-interval",
21067
+ className: "gramene-interval-form",
20936
21068
  children: [
20937
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20938
- className: "gramene-interval-toggle",
20939
- onClick: ()=>setOpen(!open),
21069
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Select, {
21070
+ size: "sm",
21071
+ className: "mb-1",
21072
+ value: effectiveTaxon,
21073
+ onChange: (ev)=>{
21074
+ setTaxonId(ev.target.value);
21075
+ setRegion('');
21076
+ },
20940
21077
  children: [
20941
- open ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {}),
20942
- " Add genomic interval"
21078
+ reference.map(genomeOption),
21079
+ anchors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
21080
+ label: "Reference genomes",
21081
+ children: anchors.map(genomeOption)
21082
+ }),
21083
+ rest.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
21084
+ label: "Other genomes",
21085
+ children: rest.map(genomeOption)
21086
+ })
20943
21087
  ]
20944
21088
  }),
20945
- open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20946
- className: "gramene-interval-form",
20947
- children: [
20948
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Select, {
20949
- size: "sm",
20950
- className: "mb-1",
20951
- value: effectiveTaxon,
20952
- onChange: (ev)=>{
20953
- setTaxonId(ev.target.value);
20954
- setRegion('');
20955
- },
21089
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
21090
+ size: "sm",
21091
+ className: "mb-1",
21092
+ value: effectiveRegion,
21093
+ onChange: (ev)=>setRegion(ev.target.value),
21094
+ children: chromosomes.map((c)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
21095
+ value: c.name,
20956
21096
  children: [
20957
- reference.map(genomeOption),
20958
- anchors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
20959
- label: "Reference genomes",
20960
- children: anchors.map(genomeOption)
20961
- }),
20962
- rest.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
20963
- label: "Other genomes",
20964
- children: rest.map(genomeOption)
20965
- })
21097
+ c.name,
21098
+ c.length ? ` (${Math.round(c.length / 1e6)} Mb)` : ''
20966
21099
  ]
20967
- }),
20968
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
21100
+ }, c.name))
21101
+ }),
21102
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21103
+ className: "gramene-interval-range",
21104
+ children: [
21105
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
20969
21106
  size: "sm",
20970
- className: "mb-1",
20971
- value: effectiveRegion,
20972
- onChange: (ev)=>setRegion(ev.target.value),
20973
- children: chromosomes.map((c)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
20974
- value: c.name,
20975
- children: [
20976
- c.name,
20977
- c.length ? ` (${Math.round(c.length / 1e6)} Mb)` : ''
20978
- ]
20979
- }, c.name))
20980
- }),
20981
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20982
- className: "gramene-interval-range",
20983
- children: [
20984
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
20985
- size: "sm",
20986
- type: "number",
20987
- min: "1",
20988
- placeholder: "start",
20989
- value: start,
20990
- onChange: (ev)=>setStart(ev.target.value)
20991
- }),
20992
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
20993
- className: "gramene-interval-dash",
20994
- children: "\u2013"
20995
- }),
20996
- /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
20997
- size: "sm",
20998
- type: "number",
20999
- min: "1",
21000
- placeholder: "end",
21001
- value: end,
21002
- onChange: (ev)=>setEnd(ev.target.value),
21003
- onKeyDown: (ev)=>{
21004
- if (ev.key === 'Enter') submit();
21005
- }
21006
- })
21007
- ]
21107
+ type: "number",
21108
+ min: "1",
21109
+ placeholder: "start",
21110
+ value: start,
21111
+ onChange: (ev)=>setStart(ev.target.value)
21008
21112
  }),
21009
- error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21010
- className: "gramene-interval-error",
21011
- children: error
21113
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
21114
+ className: "gramene-interval-dash",
21115
+ children: "\u2013"
21012
21116
  }),
21117
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21118
+ size: "sm",
21119
+ type: "number",
21120
+ min: "1",
21121
+ placeholder: "end",
21122
+ value: end,
21123
+ onChange: (ev)=>setEnd(ev.target.value),
21124
+ onKeyDown: (ev)=>{
21125
+ if (ev.key === 'Enter') submit();
21126
+ }
21127
+ })
21128
+ ]
21129
+ }),
21130
+ error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21131
+ className: "gramene-interval-error",
21132
+ children: error
21133
+ }),
21134
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21135
+ className: "gramene-interval-actions",
21136
+ children: [
21013
21137
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21014
21138
  size: "sm",
21015
21139
  variant: "primary",
21016
21140
  className: "gramene-interval-submit",
21017
21141
  disabled: !ready,
21018
21142
  onClick: submit,
21019
- children: "Add interval"
21143
+ children: submitLabel || 'Add interval'
21144
+ }),
21145
+ onCancel && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21146
+ size: "sm",
21147
+ variant: "link",
21148
+ className: "gramene-interval-cancel",
21149
+ onClick: onCancel,
21150
+ children: "cancel"
21020
21151
  })
21021
21152
  ]
21022
21153
  })
21023
21154
  ]
21024
21155
  });
21025
21156
  };
21157
+ /** The collapsible "Add genomic interval" entry at the foot of the Filters panel. */ const $909272efb41be16f$var$GenomicIntervalForm = ({ grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, onAdd: onAdd })=>{
21158
+ const [open, setOpen] = (0, $gXNCa$react.useState)(false);
21159
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21160
+ className: "gramene-interval",
21161
+ children: [
21162
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21163
+ className: "gramene-interval-toggle",
21164
+ onClick: ()=>setOpen(!open),
21165
+ children: [
21166
+ open ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {}),
21167
+ " Add genomic interval"
21168
+ ]
21169
+ }),
21170
+ open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($909272efb41be16f$export$4fa3dba3103266ea, {
21171
+ grameneMaps: grameneMaps,
21172
+ targetTaxonId: targetTaxonId,
21173
+ onSubmit: onAdd
21174
+ })
21175
+ ]
21176
+ });
21177
+ };
21026
21178
  var $909272efb41be16f$export$2e2bcd8739ae039 = $909272efb41be16f$var$GenomicIntervalForm;
21027
21179
 
21028
21180
 
@@ -21031,6 +21183,146 @@ var $909272efb41be16f$export$2e2bcd8739ae039 = $909272efb41be16f$var$GenomicInte
21031
21183
 
21032
21184
 
21033
21185
 
21186
+ /**
21187
+ * Edit one filter leaf in place, with a form shaped to what that leaf actually is:
21188
+ * a genomic interval gets the genome/chromosome/start/end fields, a `[lo TO hi]`
21189
+ * range gets two number boxes, anything else gets its raw value.
21190
+ *
21191
+ * Groups are not editable — their only parameter is the operator, which the menu
21192
+ * already exposes as "convert to".
21193
+ *
21194
+ * The plain-value case is honest but limited: many leaves carry an opaque id (a
21195
+ * pathway number, an ontology term, a saved-search hash) whose replacement a user
21196
+ * has no way to know. It is offered because a typo in a gene name or a term is
21197
+ * worth fixing without deleting and starting over.
21198
+ */ const $91efc9c46e3775e3$var$FilterEditor = ({ node: node, grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, onSave: onSave, onCancel: onCancel })=>{
21199
+ const kind = (0, $af4441dd29af05df$export$a7f4fa59930e5088)(node);
21200
+ const range = kind === 'range' ? (0, $af4441dd29af05df$export$9ee0ceaa589fc567)(node.fq_value) : null;
21201
+ const [lo, setLo] = (0, $gXNCa$react.useState)(range ? range.lo : '');
21202
+ const [hi, setHi] = (0, $gXNCa$react.useState)(range ? range.hi : '');
21203
+ const [value, setValue] = (0, $gXNCa$react.useState)(kind === 'value' ? String(node.fq_value ?? '') : '');
21204
+ if (!kind) return null;
21205
+ if (kind === 'interval') {
21206
+ const initial = (0, $af4441dd29af05df$export$67920d3e3cb3f136)(node.fq_value);
21207
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21208
+ className: "gramene-filter-editor",
21209
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $909272efb41be16f$export$4fa3dba3103266ea), {
21210
+ grameneMaps: grameneMaps,
21211
+ targetTaxonId: targetTaxonId,
21212
+ initial: initial,
21213
+ submitLabel: "Save",
21214
+ onCancel: onCancel,
21215
+ onSubmit: (iv)=>onSave((0, $af4441dd29af05df$export$634f7214da1b780a)(iv), (0, $af4441dd29af05df$export$535c09441f37845d)(iv))
21216
+ })
21217
+ });
21218
+ }
21219
+ if (kind === 'range') {
21220
+ const ok = lo !== '' && hi !== '';
21221
+ const save = ()=>{
21222
+ if (ok) onSave((0, $af4441dd29af05df$export$cd9b2ffd001c6d65)(lo, hi), `${lo}-${hi}`);
21223
+ };
21224
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21225
+ className: "gramene-filter-editor",
21226
+ children: [
21227
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21228
+ className: "gramene-interval-range",
21229
+ children: [
21230
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21231
+ size: "sm",
21232
+ value: lo,
21233
+ onChange: (e)=>setLo(e.target.value),
21234
+ placeholder: "from"
21235
+ }),
21236
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
21237
+ className: "gramene-interval-dash",
21238
+ children: "\u2013"
21239
+ }),
21240
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21241
+ size: "sm",
21242
+ value: hi,
21243
+ onChange: (e)=>setHi(e.target.value),
21244
+ placeholder: "to",
21245
+ onKeyDown: (e)=>{
21246
+ if (e.key === 'Enter') save();
21247
+ }
21248
+ })
21249
+ ]
21250
+ }),
21251
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21252
+ className: "gramene-interval-actions",
21253
+ children: [
21254
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21255
+ size: "sm",
21256
+ variant: "primary",
21257
+ className: "gramene-interval-submit",
21258
+ disabled: !ok,
21259
+ onClick: save,
21260
+ children: "Save"
21261
+ }),
21262
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21263
+ size: "sm",
21264
+ variant: "link",
21265
+ className: "gramene-interval-cancel",
21266
+ onClick: onCancel,
21267
+ children: "cancel"
21268
+ })
21269
+ ]
21270
+ })
21271
+ ]
21272
+ });
21273
+ }
21274
+ const ok = String(value).trim() !== '';
21275
+ // Keep the node's own label in step with its value, unless the label was
21276
+ // something other than the value to begin with (a suggestion's display name),
21277
+ // in which case leave it alone rather than overwriting it with a raw id.
21278
+ const save = ()=>{
21279
+ if (!ok) return;
21280
+ const v = String(value).trim();
21281
+ const nameFollowedValue = String(node.name ?? '') === String(node.fq_value ?? '');
21282
+ onSave(v, nameFollowedValue ? v : undefined);
21283
+ };
21284
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21285
+ className: "gramene-filter-editor",
21286
+ children: [
21287
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21288
+ size: "sm",
21289
+ value: value,
21290
+ onChange: (e)=>setValue(e.target.value),
21291
+ onKeyDown: (e)=>{
21292
+ if (e.key === 'Enter') save();
21293
+ }
21294
+ }),
21295
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21296
+ className: "gramene-interval-actions",
21297
+ children: [
21298
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21299
+ size: "sm",
21300
+ variant: "primary",
21301
+ className: "gramene-interval-submit",
21302
+ disabled: !ok,
21303
+ onClick: save,
21304
+ children: "Save"
21305
+ }),
21306
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21307
+ size: "sm",
21308
+ variant: "link",
21309
+ className: "gramene-interval-cancel",
21310
+ onClick: onCancel,
21311
+ children: "cancel"
21312
+ })
21313
+ ]
21314
+ })
21315
+ ]
21316
+ });
21317
+ };
21318
+ var $91efc9c46e3775e3$export$2e2bcd8739ae039 = $91efc9c46e3775e3$var$FilterEditor;
21319
+
21320
+
21321
+
21322
+
21323
+
21324
+
21325
+
21034
21326
 
21035
21327
 
21036
21328
  // "Save this view" UI — sits inside the Auth sidebar panel.
@@ -21475,6 +21767,7 @@ function $693dd8c7a5607c3a$var$logAction(action) {
21475
21767
  });
21476
21768
  }
21477
21769
  const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode, showMarked: showMarked, actions: actions })=>{
21770
+ const [editing, setEditing] = (0, $gXNCa$react.useState)(false);
21478
21771
  let classes = 'gramene-filter gramene-filter';
21479
21772
  if (node.operation) classes = `${classes}-${(0, $af4441dd29af05df$export$c8b6102be216752a)(node.operation)}`;
21480
21773
  // An expansion decorates whatever node it sits on, leaf or group — same as negate.
@@ -21534,6 +21827,14 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
21534
21827
  ]
21535
21828
  }, key++));
21536
21829
  });
21830
+ if ((0, $af4441dd29af05df$export$a7f4fa59930e5088)(node)) menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
21831
+ onClick: ()=>{
21832
+ $693dd8c7a5607c3a$var$logAction(`Edit filter`);
21833
+ actions.toggleMenu(node);
21834
+ setEditing(true);
21835
+ },
21836
+ children: "edit"
21837
+ }, key++));
21537
21838
  if (node.leftIdx > 0) {
21538
21839
  menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
21539
21840
  onClick: ()=>{
@@ -21642,12 +21943,23 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
21642
21943
  })
21643
21944
  ]
21644
21945
  });
21946
+ const editor = editing ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $91efc9c46e3775e3$export$2e2bcd8739ae039), {
21947
+ node: node,
21948
+ grameneMaps: actions.grameneMaps,
21949
+ targetTaxonId: actions.targetTaxonId,
21950
+ onCancel: ()=>setEditing(false),
21951
+ onSave: (fq_value, name)=>{
21952
+ setEditing(false);
21953
+ actions.editNode(node, fq_value, name);
21954
+ }
21955
+ }) : null;
21645
21956
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21646
21957
  className: classes,
21647
21958
  onClick: (e)=>$693dd8c7a5607c3a$var$handleClick(e, moveCopyMode, showMarked, node, actions),
21648
21959
  children: [
21649
21960
  content,
21650
21961
  menu,
21962
+ editor,
21651
21963
  children
21652
21964
  ]
21653
21965
  }, node.leftIdx);
@@ -21662,7 +21974,12 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
21662
21974
  selectTarget: props.doMoveOrCopyGrameneFilter,
21663
21975
  markTargets: props.doMarkGrameneFilterTargets,
21664
21976
  unmarkTargets: props.doUnmarkGrameneFilterTargets,
21665
- toggleMenu: props.doToggleGrameneFilterMenu
21977
+ toggleMenu: props.doToggleGrameneFilterMenu,
21978
+ editNode: props.doEditGrameneFilter,
21979
+ // Carried alongside the actions so every nested Filter can reach them without
21980
+ // threading two more props through the recursion.
21981
+ grameneMaps: props.grameneMaps,
21982
+ targetTaxonId: props.targetTaxonId
21666
21983
  };
21667
21984
  const intervalForm = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $909272efb41be16f$export$2e2bcd8739ae039), {
21668
21985
  grameneMaps: props.grameneMaps,
@@ -21710,7 +22027,7 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
21710
22027
  })
21711
22028
  });
21712
22029
  };
21713
- const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'selectGrameneMaps', 'selectTargetTaxonId', 'doAddGrameneInterval', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
22030
+ const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'selectGrameneMaps', 'selectTargetTaxonId', 'doAddGrameneInterval', 'doEditGrameneFilter', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
21714
22031
  const $693dd8c7a5607c3a$var$ResultsCmp = (props)=>{
21715
22032
  let activeViews = props.grameneViews.options;
21716
22033
  return props.grameneFilters.rightIdx > 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {