gramene-search 2.20.2 → 2.22.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
@@ -849,6 +849,43 @@ function $af4441dd29af05df$var$markSubtree(node, source, invert) {
849
849
  $af4441dd29af05df$var$markSubtree(child, source, invert);
850
850
  });
851
851
  }
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
+ }
852
889
  const $af4441dd29af05df$export$da3188ffd2da2525 = {
853
890
  orthologs: {
854
891
  label: 'orthologs',
@@ -1037,6 +1074,81 @@ const $af4441dd29af05df$var$grameneFilters = {
1037
1074
  return newState;
1038
1075
  }
1039
1076
  break;
1077
+ case 'GRAMENE_FILTER_INTERVAL_ADDED':
1078
+ {
1079
+ // Intervals accumulate into a single OR group, because ANDing two
1080
+ // disjoint intervals matches nothing — no gene sits in both. The group
1081
+ // is tagged with `category`, one of the keys viewSnapshot keeps, so a
1082
+ // restored view merges into it instead of starting a second group; were
1083
+ // that tag ever lost the worst case is an extra group, never a corrupt
1084
+ // tree.
1085
+ newState = Object.assign({}, state, {
1086
+ status: 'search',
1087
+ showMarked: true,
1088
+ searchOffset: 0
1089
+ });
1090
+ const { map: map, mapLabel: mapLabel, region: region, start: start, end: end } = payload;
1091
+ // One leaf per interval, not four. getQuery has a raw-passthrough branch
1092
+ // for a `location` leaf whose value starts with "(map:" — every other
1093
+ // branch would escape the colons — so the whole clause rides in one node
1094
+ // and the Filters panel shows one readable row per interval.
1095
+ //
1096
+ // Overlap, not containment: start <= interval end AND end >= interval
1097
+ // start, so a gene that merely crosses the boundary still matches.
1098
+ const intervalNode = {
1099
+ fq_field: 'location',
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
+ }),
1113
+ category: 'Genomic interval',
1114
+ negate: false,
1115
+ showMenu: false
1116
+ };
1117
+ let group = (newState.children || []).find((c)=>c && c.category === $af4441dd29af05df$export$6b95404109a3e2ec && Array.isArray(c.children));
1118
+ if (group) group.children.push(intervalNode);
1119
+ else newState.children.push({
1120
+ operation: 'OR',
1121
+ category: $af4441dd29af05df$export$6b95404109a3e2ec,
1122
+ negate: false,
1123
+ showMenu: false,
1124
+ children: [
1125
+ intervalNode
1126
+ ]
1127
+ });
1128
+ // Inserting a nested subtree invalidates the hand-computed indices the
1129
+ // append actions rely on, so renumber the whole tree.
1130
+ $af4441dd29af05df$var$reindexTree(newState, 0);
1131
+ $af4441dd29af05df$var$markSubtree(newState, newState, false);
1132
+ return newState;
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
+ }
1040
1152
  case 'GRAMENE_FILTER_EXPANSION_SET':
1041
1153
  {
1042
1154
  // Setting or clearing an expansion is just a field on the node — the tree
@@ -1353,6 +1465,47 @@ const $af4441dd29af05df$var$grameneFilters = {
1353
1465
  ]
1354
1466
  });
1355
1467
  },
1468
+ // Add one genomic interval. Accumulates into a single OR group; see the
1469
+ // GRAMENE_FILTER_INTERVAL_ADDED reducer for why they cannot be ANDed.
1470
+ doAddGrameneInterval: (interval)=>({ dispatch: dispatch })=>{
1471
+ dispatch({
1472
+ type: 'BATCH_ACTIONS',
1473
+ actions: [
1474
+ {
1475
+ type: 'GRAMENE_SEARCH_CLEARED'
1476
+ },
1477
+ {
1478
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1479
+ },
1480
+ {
1481
+ type: 'GRAMENE_FILTER_INTERVAL_ADDED',
1482
+ payload: interval
1483
+ }
1484
+ ]
1485
+ });
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
+ },
1356
1509
  doToggleGrameneFilterMenu: (node)=>({ dispatch: dispatch })=>{
1357
1510
  dispatch({
1358
1511
  type: 'GRAMENE_FILTER_MENU_TOGGLED',
@@ -20789,6 +20942,350 @@ var $7d7b7dad5e71dfd3$export$2e2bcd8739ae039 = $7d7b7dad5e71dfd3$var$Facets;
20789
20942
 
20790
20943
 
20791
20944
 
20945
+ // Sequences that aren't a placed chromosome. Excluded from the dropdown because
20946
+ // an interval on them is not something a user can reason about positionally.
20947
+ // Organelles (C, M, Mt, Pt, mitochondrion_genome) are deliberately kept — they
20948
+ // are real, coordinate-addressable sequences.
20949
+ const $909272efb41be16f$var$UNPLACED = /^(unanchored|u)$/i;
20950
+ function $909272efb41be16f$export$ac97d17982e0cb21(grameneMaps, targetTaxonId) {
20951
+ const genomes = Object.values(grameneMaps || {}).filter((m)=>m && m._id && m.regions);
20952
+ const byLabel = (a, b)=>String(a.display_name || '').localeCompare(String(b.display_name || ''));
20953
+ const reference = genomes.filter((m)=>String(m.taxon_id) === String(targetTaxonId));
20954
+ const refIds = new Set(reference.map((m)=>m.taxon_id));
20955
+ const anchors = genomes.filter((m)=>m.is_anchor && !refIds.has(m.taxon_id)).sort(byLabel);
20956
+ const anchorIds = new Set(anchors.map((m)=>m.taxon_id));
20957
+ const rest = genomes.filter((m)=>!refIds.has(m.taxon_id) && !anchorIds.has(m.taxon_id)).sort(byLabel);
20958
+ return {
20959
+ reference: reference,
20960
+ anchors: anchors,
20961
+ rest: rest
20962
+ };
20963
+ }
20964
+ function $909272efb41be16f$export$94955a075dcb14b2(genome) {
20965
+ const regions = genome && genome.regions || {};
20966
+ const names = regions.names || [];
20967
+ const lengths = regions.lengths || [];
20968
+ return names.map((name, i)=>({
20969
+ name: name,
20970
+ length: lengths[i]
20971
+ })).filter((r)=>!$909272efb41be16f$var$UNPLACED.test(String(r.name)));
20972
+ }
20973
+ const $909272efb41be16f$export$4fa3dba3103266ea = ({ grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, initial: initial, submitLabel: submitLabel, onSubmit: onSubmit, onCancel: onCancel })=>{
20974
+ const [taxonId, setTaxonId] = (0, $gXNCa$react.useState)('');
20975
+ const [region, setRegion] = (0, $gXNCa$react.useState)(initial ? String(initial.region) : '');
20976
+ const [start, setStart] = (0, $gXNCa$react.useState)(initial ? String(initial.start) : '');
20977
+ const [end, setEnd] = (0, $gXNCa$react.useState)(initial ? String(initial.end) : '');
20978
+ const { reference: reference, anchors: anchors, rest: rest } = (0, $gXNCa$react.useMemo)(()=>$909272efb41be16f$export$ac97d17982e0cb21(grameneMaps, targetTaxonId), [
20979
+ grameneMaps,
20980
+ targetTaxonId
20981
+ ]);
20982
+ // An edit starts on the filter's own genome; a new interval starts on the
20983
+ // site's reference. `initial.map` is an assembly accession, so find its genome.
20984
+ const initialTaxon = (0, $gXNCa$react.useMemo)(()=>{
20985
+ if (!initial) return '';
20986
+ const hit = Object.values(grameneMaps || {}).find((m)=>m && m._id === initial.map);
20987
+ return hit ? String(hit.taxon_id) : '';
20988
+ }, [
20989
+ initial,
20990
+ grameneMaps
20991
+ ]);
20992
+ const effectiveTaxon = taxonId || initialTaxon || reference[0] && String(reference[0].taxon_id) || '';
20993
+ const genome = effectiveTaxon ? (grameneMaps || {})[effectiveTaxon] : null;
20994
+ const chromosomes = (0, $gXNCa$react.useMemo)(()=>$909272efb41be16f$export$94955a075dcb14b2(genome), [
20995
+ genome
20996
+ ]);
20997
+ const effectiveRegion = region || chromosomes[0] && chromosomes[0].name || '';
20998
+ const chrLength = (chromosomes.find((c)=>String(c.name) === String(effectiveRegion)) || {}).length;
20999
+ const s = start === '' ? null : Number(start);
21000
+ const e = end === '' ? null : Number(end);
21001
+ // Validated against the assembly's own region lengths, so the message can name
21002
+ // the actual limit rather than just refusing.
21003
+ const error = (()=>{
21004
+ if (!genome || s === null || e === null) return null;
21005
+ if (!Number.isFinite(s) || !Number.isFinite(e)) return 'Positions must be numbers.';
21006
+ if (s < 1 || e < 1) return 'Positions start at 1.';
21007
+ if (s > e) return 'Start must not be greater than end.';
21008
+ if (chrLength && e > chrLength) return `${effectiveRegion} is ${chrLength.toLocaleString()} bp; end is beyond it.`;
21009
+ return null;
21010
+ })();
21011
+ const ready = !!genome && !!effectiveRegion && s !== null && e !== null && !error;
21012
+ const submit = ()=>{
21013
+ if (!ready) return;
21014
+ onSubmit({
21015
+ map: genome._id,
21016
+ mapLabel: genome.display_name || genome._id,
21017
+ region: effectiveRegion,
21018
+ start: s,
21019
+ end: e
21020
+ });
21021
+ if (!initial) {
21022
+ setStart('');
21023
+ setEnd('');
21024
+ }
21025
+ };
21026
+ const genomeOption = (m)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("option", {
21027
+ value: m.taxon_id,
21028
+ children: m.display_name || m.system_name
21029
+ }, m.taxon_id);
21030
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21031
+ className: "gramene-interval-form",
21032
+ children: [
21033
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Form).Select, {
21034
+ size: "sm",
21035
+ className: "mb-1",
21036
+ value: effectiveTaxon,
21037
+ onChange: (ev)=>{
21038
+ setTaxonId(ev.target.value);
21039
+ setRegion('');
21040
+ },
21041
+ children: [
21042
+ reference.map(genomeOption),
21043
+ anchors.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
21044
+ label: "Reference genomes",
21045
+ children: anchors.map(genomeOption)
21046
+ }),
21047
+ rest.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("optgroup", {
21048
+ label: "Other genomes",
21049
+ children: rest.map(genomeOption)
21050
+ })
21051
+ ]
21052
+ }),
21053
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Select, {
21054
+ size: "sm",
21055
+ className: "mb-1",
21056
+ value: effectiveRegion,
21057
+ onChange: (ev)=>setRegion(ev.target.value),
21058
+ children: chromosomes.map((c)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("option", {
21059
+ value: c.name,
21060
+ children: [
21061
+ c.name,
21062
+ c.length ? ` (${Math.round(c.length / 1e6)} Mb)` : ''
21063
+ ]
21064
+ }, c.name))
21065
+ }),
21066
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21067
+ className: "gramene-interval-range",
21068
+ children: [
21069
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21070
+ size: "sm",
21071
+ type: "number",
21072
+ min: "1",
21073
+ placeholder: "start",
21074
+ value: start,
21075
+ onChange: (ev)=>setStart(ev.target.value)
21076
+ }),
21077
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
21078
+ className: "gramene-interval-dash",
21079
+ children: "\u2013"
21080
+ }),
21081
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21082
+ size: "sm",
21083
+ type: "number",
21084
+ min: "1",
21085
+ placeholder: "end",
21086
+ value: end,
21087
+ onChange: (ev)=>setEnd(ev.target.value),
21088
+ onKeyDown: (ev)=>{
21089
+ if (ev.key === 'Enter') submit();
21090
+ }
21091
+ })
21092
+ ]
21093
+ }),
21094
+ error && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21095
+ className: "gramene-interval-error",
21096
+ children: error
21097
+ }),
21098
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21099
+ className: "gramene-interval-actions",
21100
+ children: [
21101
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21102
+ size: "sm",
21103
+ variant: "primary",
21104
+ className: "gramene-interval-submit",
21105
+ disabled: !ready,
21106
+ onClick: submit,
21107
+ children: submitLabel || 'Add interval'
21108
+ }),
21109
+ onCancel && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21110
+ size: "sm",
21111
+ variant: "link",
21112
+ className: "gramene-interval-cancel",
21113
+ onClick: onCancel,
21114
+ children: "cancel"
21115
+ })
21116
+ ]
21117
+ })
21118
+ ]
21119
+ });
21120
+ };
21121
+ /** The collapsible "Add genomic interval" entry at the foot of the Filters panel. */ const $909272efb41be16f$var$GenomicIntervalForm = ({ grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, onAdd: onAdd })=>{
21122
+ const [open, setOpen] = (0, $gXNCa$react.useState)(false);
21123
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21124
+ className: "gramene-interval",
21125
+ children: [
21126
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21127
+ className: "gramene-interval-toggle",
21128
+ onClick: ()=>setOpen(!open),
21129
+ children: [
21130
+ open ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronDown), {}) : /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsChevronRight), {}),
21131
+ " Add genomic interval"
21132
+ ]
21133
+ }),
21134
+ open && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($909272efb41be16f$export$4fa3dba3103266ea, {
21135
+ grameneMaps: grameneMaps,
21136
+ targetTaxonId: targetTaxonId,
21137
+ onSubmit: onAdd
21138
+ })
21139
+ ]
21140
+ });
21141
+ };
21142
+ var $909272efb41be16f$export$2e2bcd8739ae039 = $909272efb41be16f$var$GenomicIntervalForm;
21143
+
21144
+
21145
+
21146
+
21147
+
21148
+
21149
+
21150
+ /**
21151
+ * Edit one filter leaf in place, with a form shaped to what that leaf actually is:
21152
+ * a genomic interval gets the genome/chromosome/start/end fields, a `[lo TO hi]`
21153
+ * range gets two number boxes, anything else gets its raw value.
21154
+ *
21155
+ * Groups are not editable — their only parameter is the operator, which the menu
21156
+ * already exposes as "convert to".
21157
+ *
21158
+ * The plain-value case is honest but limited: many leaves carry an opaque id (a
21159
+ * pathway number, an ontology term, a saved-search hash) whose replacement a user
21160
+ * has no way to know. It is offered because a typo in a gene name or a term is
21161
+ * worth fixing without deleting and starting over.
21162
+ */ const $91efc9c46e3775e3$var$FilterEditor = ({ node: node, grameneMaps: grameneMaps, targetTaxonId: targetTaxonId, onSave: onSave, onCancel: onCancel })=>{
21163
+ const kind = (0, $af4441dd29af05df$export$a7f4fa59930e5088)(node);
21164
+ const range = kind === 'range' ? (0, $af4441dd29af05df$export$9ee0ceaa589fc567)(node.fq_value) : null;
21165
+ const [lo, setLo] = (0, $gXNCa$react.useState)(range ? range.lo : '');
21166
+ const [hi, setHi] = (0, $gXNCa$react.useState)(range ? range.hi : '');
21167
+ const [value, setValue] = (0, $gXNCa$react.useState)(kind === 'value' ? String(node.fq_value ?? '') : '');
21168
+ if (!kind) return null;
21169
+ if (kind === 'interval') {
21170
+ const initial = (0, $af4441dd29af05df$export$67920d3e3cb3f136)(node.fq_value);
21171
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21172
+ className: "gramene-filter-editor",
21173
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $909272efb41be16f$export$4fa3dba3103266ea), {
21174
+ grameneMaps: grameneMaps,
21175
+ targetTaxonId: targetTaxonId,
21176
+ initial: initial,
21177
+ submitLabel: "Save",
21178
+ onCancel: onCancel,
21179
+ onSubmit: (iv)=>onSave((0, $af4441dd29af05df$export$634f7214da1b780a)(iv), (0, $af4441dd29af05df$export$535c09441f37845d)(iv))
21180
+ })
21181
+ });
21182
+ }
21183
+ if (kind === 'range') {
21184
+ const ok = lo !== '' && hi !== '';
21185
+ const save = ()=>{
21186
+ if (ok) onSave((0, $af4441dd29af05df$export$cd9b2ffd001c6d65)(lo, hi), `${lo}-${hi}`);
21187
+ };
21188
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21189
+ className: "gramene-filter-editor",
21190
+ children: [
21191
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21192
+ className: "gramene-interval-range",
21193
+ children: [
21194
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21195
+ size: "sm",
21196
+ value: lo,
21197
+ onChange: (e)=>setLo(e.target.value),
21198
+ placeholder: "from"
21199
+ }),
21200
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
21201
+ className: "gramene-interval-dash",
21202
+ children: "\u2013"
21203
+ }),
21204
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21205
+ size: "sm",
21206
+ value: hi,
21207
+ onChange: (e)=>setHi(e.target.value),
21208
+ placeholder: "to",
21209
+ onKeyDown: (e)=>{
21210
+ if (e.key === 'Enter') save();
21211
+ }
21212
+ })
21213
+ ]
21214
+ }),
21215
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21216
+ className: "gramene-interval-actions",
21217
+ children: [
21218
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21219
+ size: "sm",
21220
+ variant: "primary",
21221
+ className: "gramene-interval-submit",
21222
+ disabled: !ok,
21223
+ onClick: save,
21224
+ children: "Save"
21225
+ }),
21226
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21227
+ size: "sm",
21228
+ variant: "link",
21229
+ className: "gramene-interval-cancel",
21230
+ onClick: onCancel,
21231
+ children: "cancel"
21232
+ })
21233
+ ]
21234
+ })
21235
+ ]
21236
+ });
21237
+ }
21238
+ const ok = String(value).trim() !== '';
21239
+ // Keep the node's own label in step with its value, unless the label was
21240
+ // something other than the value to begin with (a suggestion's display name),
21241
+ // in which case leave it alone rather than overwriting it with a raw id.
21242
+ const save = ()=>{
21243
+ if (!ok) return;
21244
+ const v = String(value).trim();
21245
+ const nameFollowedValue = String(node.name ?? '') === String(node.fq_value ?? '');
21246
+ onSave(v, nameFollowedValue ? v : undefined);
21247
+ };
21248
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21249
+ className: "gramene-filter-editor",
21250
+ children: [
21251
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Form).Control, {
21252
+ size: "sm",
21253
+ value: value,
21254
+ onChange: (e)=>setValue(e.target.value),
21255
+ onKeyDown: (e)=>{
21256
+ if (e.key === 'Enter') save();
21257
+ }
21258
+ }),
21259
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21260
+ className: "gramene-interval-actions",
21261
+ children: [
21262
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21263
+ size: "sm",
21264
+ variant: "primary",
21265
+ className: "gramene-interval-submit",
21266
+ disabled: !ok,
21267
+ onClick: save,
21268
+ children: "Save"
21269
+ }),
21270
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
21271
+ size: "sm",
21272
+ variant: "link",
21273
+ className: "gramene-interval-cancel",
21274
+ onClick: onCancel,
21275
+ children: "cancel"
21276
+ })
21277
+ ]
21278
+ })
21279
+ ]
21280
+ });
21281
+ };
21282
+ var $91efc9c46e3775e3$export$2e2bcd8739ae039 = $91efc9c46e3775e3$var$FilterEditor;
21283
+
21284
+
21285
+
21286
+
21287
+
21288
+
20792
21289
 
20793
21290
 
20794
21291
 
@@ -21234,6 +21731,7 @@ function $693dd8c7a5607c3a$var$logAction(action) {
21234
21731
  });
21235
21732
  }
21236
21733
  const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode, showMarked: showMarked, actions: actions })=>{
21734
+ const [editing, setEditing] = (0, $gXNCa$react.useState)(false);
21237
21735
  let classes = 'gramene-filter gramene-filter';
21238
21736
  if (node.operation) classes = `${classes}-${(0, $af4441dd29af05df$export$c8b6102be216752a)(node.operation)}`;
21239
21737
  // An expansion decorates whatever node it sits on, leaf or group — same as negate.
@@ -21293,6 +21791,14 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
21293
21791
  ]
21294
21792
  }, key++));
21295
21793
  });
21794
+ if ((0, $af4441dd29af05df$export$a7f4fa59930e5088)(node)) menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
21795
+ onClick: ()=>{
21796
+ $693dd8c7a5607c3a$var$logAction(`Edit filter`);
21797
+ actions.toggleMenu(node);
21798
+ setEditing(true);
21799
+ },
21800
+ children: "edit"
21801
+ }, key++));
21296
21802
  if (node.leftIdx > 0) {
21297
21803
  menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
21298
21804
  onClick: ()=>{
@@ -21401,12 +21907,23 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
21401
21907
  })
21402
21908
  ]
21403
21909
  });
21910
+ const editor = editing ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $91efc9c46e3775e3$export$2e2bcd8739ae039), {
21911
+ node: node,
21912
+ grameneMaps: actions.grameneMaps,
21913
+ targetTaxonId: actions.targetTaxonId,
21914
+ onCancel: ()=>setEditing(false),
21915
+ onSave: (fq_value, name)=>{
21916
+ setEditing(false);
21917
+ actions.editNode(node, fq_value, name);
21918
+ }
21919
+ }) : null;
21404
21920
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
21405
21921
  className: classes,
21406
21922
  onClick: (e)=>$693dd8c7a5607c3a$var$handleClick(e, moveCopyMode, showMarked, node, actions),
21407
21923
  children: [
21408
21924
  content,
21409
21925
  menu,
21926
+ editor,
21410
21927
  children
21411
21928
  ]
21412
21929
  }, node.leftIdx);
@@ -21421,12 +21938,22 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
21421
21938
  selectTarget: props.doMoveOrCopyGrameneFilter,
21422
21939
  markTargets: props.doMarkGrameneFilterTargets,
21423
21940
  unmarkTargets: props.doUnmarkGrameneFilterTargets,
21424
- toggleMenu: props.doToggleGrameneFilterMenu
21941
+ toggleMenu: props.doToggleGrameneFilterMenu,
21942
+ editNode: props.doEditGrameneFilter,
21943
+ // Carried alongside the actions so every nested Filter can reach them without
21944
+ // threading two more props through the recursion.
21945
+ grameneMaps: props.grameneMaps,
21946
+ targetTaxonId: props.targetTaxonId
21425
21947
  };
21948
+ const intervalForm = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $909272efb41be16f$export$2e2bcd8739ae039), {
21949
+ grameneMaps: props.grameneMaps,
21950
+ targetTaxonId: props.targetTaxonId,
21951
+ onAdd: props.doAddGrameneInterval
21952
+ });
21426
21953
  const containerClass = props.configuration.id === 'sorghum' ? 'sorghumbase-filter-container' : 'gramene-filter-container';
21427
21954
  if (props.grameneFilters.rightIdx > 1) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21428
21955
  className: containerClass,
21429
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$CollapsibleSection, {
21956
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)($693dd8c7a5607c3a$var$CollapsibleSection, {
21430
21957
  title: "Filters",
21431
21958
  extra: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
21432
21959
  style: {
@@ -21439,26 +21966,32 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
21439
21966
  },
21440
21967
  children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reacticonsbs.BsTrash), {})
21441
21968
  }),
21442
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$Filter, {
21443
- node: props.grameneFilters,
21444
- moveCopyMode: props.grameneFilters.moveCopyMode,
21445
- showMarked: props.grameneFilters.showMarked,
21446
- actions: actions
21447
- })
21969
+ children: [
21970
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$Filter, {
21971
+ node: props.grameneFilters,
21972
+ moveCopyMode: props.grameneFilters.moveCopyMode,
21973
+ showMarked: props.grameneFilters.showMarked,
21974
+ actions: actions
21975
+ }),
21976
+ intervalForm
21977
+ ]
21448
21978
  })
21449
21979
  });
21450
21980
  else return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21451
21981
  className: containerClass,
21452
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$CollapsibleSection, {
21982
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)($693dd8c7a5607c3a$var$CollapsibleSection, {
21453
21983
  title: "Filters",
21454
- children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21455
- className: "gramene-filter gramene-filter-AND",
21456
- children: "No filters defined"
21457
- })
21984
+ children: [
21985
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
21986
+ className: "gramene-filter gramene-filter-AND",
21987
+ children: "No filters defined"
21988
+ }),
21989
+ intervalForm
21990
+ ]
21458
21991
  })
21459
21992
  });
21460
21993
  };
21461
- const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
21994
+ 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);
21462
21995
  const $693dd8c7a5607c3a$var$ResultsCmp = (props)=>{
21463
21996
  let activeViews = props.grameneViews.options;
21464
21997
  return props.grameneFilters.rightIdx > 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {