gramene-search 2.15.7 → 2.17.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
@@ -850,6 +850,49 @@ function $af4441dd29af05df$var$markSubtree(node, source, invert) {
850
850
  $af4441dd29af05df$var$markSubtree(child, source, invert);
851
851
  });
852
852
  }
853
+ const $af4441dd29af05df$export$da3188ffd2da2525 = {
854
+ orthologs: {
855
+ label: 'orthologs',
856
+ // Edge fields: a seed gene's homology__all_orthologs values are the ids of its
857
+ // orthologs. Same field already queried directly in bundles/api.js.
858
+ from: 'homology__all_orthologs',
859
+ to: 'id'
860
+ },
861
+ paralogs: {
862
+ label: 'paralogs',
863
+ // Within-species, so this stays inside the seed's own genome — a much smaller
864
+ // expansion than orthologs (11 vs 70 for msd2).
865
+ from: 'homology__within_species_paralog',
866
+ to: 'id'
867
+ },
868
+ neighborhood: {
869
+ label: "neighborhood (\xb110)",
870
+ // Same edge pair TBrowse walks for its neighborhood zone (see
871
+ // components/results/details/Homology.js). Unlike the homology fields these are
872
+ // numeric: a gene's compara_neighbors_10 lists the compara_idx values of the 10
873
+ // genes either side, matched against compara_idx_multi on the target. TBrowse
874
+ // omits returnRoot, which Solr defaults to true — the same as every expansion
875
+ // here — so a seed gene expands to 21: itself plus 20 neighbours.
876
+ from: 'compara_neighbors_10',
877
+ to: 'compara_idx_multi'
878
+ }
879
+ };
880
+ function $af4441dd29af05df$export$a6dc3fef1cecdf28(node) {
881
+ const type = node && node.expand;
882
+ return typeof type === 'string' && type ? type : null;
883
+ }
884
+ function $af4441dd29af05df$export$c8b6102be216752a(token) {
885
+ return String(token).replace(/[^A-Za-z0-9_-]/g, '-');
886
+ }
887
+ function $af4441dd29af05df$export$fb4df7b076406c76(subquery) {
888
+ return String(subquery).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
889
+ }
890
+ function $af4441dd29af05df$export$90cfa1f3fbcb6430(node) {
891
+ const type = $af4441dd29af05df$export$a6dc3fef1cecdf28(node);
892
+ if (!type) return null;
893
+ const spec = $af4441dd29af05df$export$da3188ffd2da2525[type];
894
+ return `EXPAND: ${spec ? spec.label : type}`;
895
+ }
853
896
  function $af4441dd29af05df$var$reindexTree(node, idx) {
854
897
  node.leftIdx = idx;
855
898
  idx++;
@@ -988,11 +1031,33 @@ const $af4441dd29af05df$var$grameneFilters = {
988
1031
  searchOffset: 0
989
1032
  });
990
1033
  let node = $af4441dd29af05df$var$findNodeWithLeftIdx(newState, payload.leftIdx);
991
- node.operation = node.operation === 'AND' ? 'OR' : 'AND';
1034
+ // Set an explicit operation when one is supplied; otherwise keep the
1035
+ // historical AND<->OR flip for callers that just pass the node.
1036
+ node.operation = payload.newOperation ? payload.newOperation : node.operation === 'AND' ? 'OR' : 'AND';
992
1037
  $af4441dd29af05df$var$markSubtree(newState, node, false);
993
1038
  return newState;
994
1039
  }
995
1040
  break;
1041
+ case 'GRAMENE_FILTER_EXPANSION_SET':
1042
+ {
1043
+ // Setting or clearing an expansion is just a field on the node — the tree
1044
+ // shape is untouched, so there is nothing to reindex and no special case
1045
+ // for the root. `payload.expand` of null/undefined removes it.
1046
+ newState = Object.assign({}, state, {
1047
+ status: 'search',
1048
+ showMarked: true,
1049
+ searchOffset: 0
1050
+ });
1051
+ let node = $af4441dd29af05df$var$findNodeWithLeftIdx(newState, payload.leftIdx);
1052
+ if (!node) break;
1053
+ // Leave showMenu alone, as GRAMENE_FILTER_NEGATED does. The radios are a
1054
+ // setting rather than a command, so the menu stays open and the selection
1055
+ // updates in place — you can switch type or clear without reopening it.
1056
+ if (payload.expand) node.expand = payload.expand;
1057
+ else delete node.expand;
1058
+ $af4441dd29af05df$var$markSubtree(newState, node, false);
1059
+ return newState;
1060
+ }
996
1061
  case 'GRAMENE_FILTER_MOVED':
997
1062
  {
998
1063
  newState = Object.assign({}, state, {
@@ -1163,6 +1228,48 @@ const $af4441dd29af05df$var$grameneFilters = {
1163
1228
  ]
1164
1229
  });
1165
1230
  },
1231
+ // Expand a node (leaf or group) along a relationship, e.g. "orthologs of this".
1232
+ doExpandGrameneFilter: (filter, type)=>({ dispatch: dispatch })=>{
1233
+ dispatch({
1234
+ type: 'BATCH_ACTIONS',
1235
+ actions: [
1236
+ {
1237
+ type: 'GRAMENE_SEARCH_CLEARED'
1238
+ },
1239
+ {
1240
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1241
+ },
1242
+ {
1243
+ type: 'GRAMENE_FILTER_EXPANSION_SET',
1244
+ payload: {
1245
+ leftIdx: filter.leftIdx,
1246
+ expand: type
1247
+ }
1248
+ }
1249
+ ]
1250
+ });
1251
+ },
1252
+ // Clear a node's expansion, leaving the node and its filters in place.
1253
+ doRemoveGrameneExpansion: (filter)=>({ dispatch: dispatch })=>{
1254
+ dispatch({
1255
+ type: 'BATCH_ACTIONS',
1256
+ actions: [
1257
+ {
1258
+ type: 'GRAMENE_SEARCH_CLEARED'
1259
+ },
1260
+ {
1261
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1262
+ },
1263
+ {
1264
+ type: 'GRAMENE_FILTER_EXPANSION_SET',
1265
+ payload: {
1266
+ leftIdx: filter.leftIdx,
1267
+ expand: null
1268
+ }
1269
+ }
1270
+ ]
1271
+ });
1272
+ },
1166
1273
  doMoveOrCopyGrameneFilter: (target)=>({ dispatch: dispatch, getState: getState })=>{
1167
1274
  const state = getState();
1168
1275
  if (state.grameneFilters.moveCopyMode) {
@@ -1301,18 +1408,45 @@ const $af4441dd29af05df$var$grameneFilters = {
1301
1408
  const isQuery = new RegExp(/\([a-zA-Z0-9_]+:[a-zA-Z0-9_]+\s/);
1302
1409
  const isRegionQuery = new RegExp(/^\(map:/);
1303
1410
  const idWithColon = new RegExp(/^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$/);
1411
+ // The node's own clause, before negation and before any expansion.
1412
+ function ownQuery(node) {
1413
+ if (node.hasOwnProperty('children')) {
1414
+ // Only AND and OR are valid infix operators. Anything else — a token from a
1415
+ // newer client arriving via ?filters= or a saved view — would otherwise be
1416
+ // interpolated raw and produce a Solr 400, which surfaces as blank results
1417
+ // across every view rather than as an error.
1418
+ const op = node.operation === 'AND' || node.operation === 'OR' ? node.operation : 'OR';
1419
+ const kids = Array.isArray(node.children) ? node.children : [];
1420
+ return `(${kids.map((c)=>getQuery(c)).sort().join(` ${op} `)})`;
1421
+ }
1422
+ // this node is a suggestion
1423
+ if (node.fq_field === 'location' && isRegionQuery.test(node.fq_value)) return `${node.fq_value}`;
1424
+ if (isQuery.test(node.fq_value)) return `${node.fq_value.replace(/:/g, '\\:')}`;
1425
+ if (hasSpaces.test(node.fq_value)) return `${node.fq_field}:"${node.fq_value}"`;
1426
+ else if (idWithColon.test(node.fq_value)) return `${node.fq_field}:${node.fq_value.replace(/:/g, '\\:')}`;
1427
+ else return `${node.fq_field}:${node.fq_value}`;
1428
+ }
1304
1429
  function getQuery(node) {
1305
1430
  const negate = node.negate ? 'NOT ' : '';
1306
- if (node.hasOwnProperty('children')) // do some recursion
1307
- return `${negate}(${node.children.map((c)=>getQuery(c)).sort().join(` ${node.operation} `)})`;
1308
- else {
1309
- // this node is a suggestion
1310
- if (node.fq_field === 'location' && isRegionQuery.test(node.fq_value)) return `${negate}${node.fq_value}`;
1311
- if (isQuery.test(node.fq_value)) return `${negate}${node.fq_value.replace(/:/g, '\\:')}`;
1312
- if (hasSpaces.test(node.fq_value)) return `${negate}${node.fq_field}:"${node.fq_value}"`;
1313
- else if (idWithColon.test(node.fq_value)) return `${negate}${node.fq_field}:${node.fq_value.replace(/:/g, '\\:')}`;
1314
- else return `${negate}${node.fq_field}:${node.fq_value}`;
1315
- }
1431
+ const clause = ownQuery(node);
1432
+ const expType = $af4441dd29af05df$export$a6dc3fef1cecdf28(node);
1433
+ // No expansion on this node: unchanged from before.
1434
+ if (!expType) return `${negate}${clause}`;
1435
+ const spec = $af4441dd29af05df$export$da3188ffd2da2525[expType];
1436
+ // Unknown expansion type — e.g. a link shared from a newer client. Degrade to
1437
+ // the un-expanded clause rather than emitting something broken.
1438
+ if (!spec) return `${negate}${clause}`;
1439
+ // An expansion of nothing must match nothing; '*:*' here would turn an empty
1440
+ // group into "every gene in the index".
1441
+ if (!clause || clause === '()') return `${negate}(*:* AND NOT *:*)`;
1442
+ // The braces MUST stay inside a quoted `_query_` value. A bare `{!graph ...}`
1443
+ // in `q` is unreliable and fails *silently* — observed returning 0 rows in
1444
+ // some shapes and millions in others, always HTTP 200, depending on the seed
1445
+ // and the endpoint in front of Solr. Don't factor `graph` out to module scope
1446
+ // where it could be used somewhere that isn't wrapped this way.
1447
+ const graph = `{!graph from=${spec.from} to=${spec.to} maxDepth=1 returnRoot=true}`;
1448
+ // Negation applies to the expanded set: "NOT an ortholog of msd2".
1449
+ return `${negate}(_query_:"${graph}${$af4441dd29af05df$export$fb4df7b076406c76(clause)}")`;
1316
1450
  }
1317
1451
  if (state.grameneFilters.rightIdx === 1) return '*:*';
1318
1452
  return `*:* AND (${getQuery(state.grameneFilters)})`;
@@ -6270,6 +6404,7 @@ const $472fe3745b238881$var$FILTER_NODE_KEEP = [
6270
6404
  'rightIdx',
6271
6405
  'operation',
6272
6406
  'negate',
6407
+ 'expand',
6273
6408
  'fq_field',
6274
6409
  'fq_value',
6275
6410
  'name',
@@ -6279,6 +6414,7 @@ const $472fe3745b238881$var$FILTER_NODE_KEEP = [
6279
6414
  const $472fe3745b238881$var$FILTER_ROOT_KEEP = [
6280
6415
  'operation',
6281
6416
  'negate',
6417
+ 'expand',
6282
6418
  'leftIdx',
6283
6419
  'rightIdx',
6284
6420
  'children',
@@ -20063,6 +20199,7 @@ var $7d7b7dad5e71dfd3$export$2e2bcd8739ae039 = $7d7b7dad5e71dfd3$var$Facets;
20063
20199
 
20064
20200
 
20065
20201
 
20202
+
20066
20203
  // "Save this view" UI — sits inside the Auth sidebar panel.
20067
20204
  //
20068
20205
  // Sign-in-gated by Auth.js (we only mount this when `user` is truthy). Opens
@@ -20506,7 +20643,10 @@ function $693dd8c7a5607c3a$var$logAction(action) {
20506
20643
  }
20507
20644
  const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode, showMarked: showMarked, actions: actions })=>{
20508
20645
  let classes = 'gramene-filter gramene-filter';
20509
- if (node.operation) classes = `${classes}-${node.operation}`;
20646
+ if (node.operation) classes = `${classes}-${(0, $af4441dd29af05df$export$c8b6102be216752a)(node.operation)}`;
20647
+ // An expansion decorates whatever node it sits on, leaf or group — same as negate.
20648
+ const expType = (0, $af4441dd29af05df$export$a6dc3fef1cecdf28)(node);
20649
+ if (expType) classes = `${classes} gramene-filter-EXPAND gramene-filter-EXPAND-${(0, $af4441dd29af05df$export$c8b6102be216752a)(expType)}`;
20510
20650
  if (node.negate) classes = `${classes} gramene-filter-NOT`;
20511
20651
  if (showMarked && node.marked) classes = `${classes} gramene-filter-marked`;
20512
20652
  let content = '';
@@ -20527,6 +20667,40 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20527
20667
  })
20528
20668
  ]
20529
20669
  }, key++));
20670
+ // A node carries at most one expansion, so the types are radios rather than
20671
+ // separate commands. Any node can carry one, leaf or group — attaching it to a
20672
+ // group means "expand whatever this group matches".
20673
+ menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
20674
+ className: "gramene-filter-menu-label",
20675
+ children: "expand search"
20676
+ }, key++));
20677
+ Object.keys((0, $af4441dd29af05df$export$da3188ffd2da2525)).forEach((type)=>{
20678
+ const checked = expType === type;
20679
+ menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
20680
+ className: "gramene-filter-menu-radio",
20681
+ onClick: ()=>{
20682
+ // Re-picking the active type clears it — the only way to remove an
20683
+ // expansion from the root, which has no `delete`.
20684
+ if (checked) {
20685
+ $693dd8c7a5607c3a$var$logAction(`Remove expansion`);
20686
+ actions.removeExpansion(node);
20687
+ } else {
20688
+ $693dd8c7a5607c3a$var$logAction(`Expand by ${type}`);
20689
+ actions.expand(node, type);
20690
+ }
20691
+ },
20692
+ children: [
20693
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
20694
+ type: "radio",
20695
+ readOnly: true,
20696
+ checked: checked,
20697
+ onChange: ()=>{},
20698
+ name: `gramene-expand-${node.leftIdx}`
20699
+ }),
20700
+ (0, $af4441dd29af05df$export$da3188ffd2da2525)[type].label
20701
+ ]
20702
+ }, key++));
20703
+ });
20530
20704
  if (node.leftIdx > 0) {
20531
20705
  menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
20532
20706
  onClick: ()=>{
@@ -20596,6 +20770,13 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20596
20770
  })
20597
20771
  });
20598
20772
  }
20773
+ // Shown above the node's own label, since the expansion applies to whatever that
20774
+ // node resolves to rather than replacing it.
20775
+ const expBadge = expType ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
20776
+ className: "gramene-filter-expansion",
20777
+ onClick: ()=>actions.toggleMenu(node),
20778
+ children: (0, $af4441dd29af05df$export$90cfa1f3fbcb6430)(node)
20779
+ }) : null;
20599
20780
  if (node.operation) {
20600
20781
  children = node.children.map((child, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$Filter, {
20601
20782
  moveCopyMode: moveCopyMode,
@@ -20605,6 +20786,7 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20605
20786
  }, idx));
20606
20787
  content = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20607
20788
  children: [
20789
+ expBadge,
20608
20790
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
20609
20791
  className: "gramene-filter-operation",
20610
20792
  onClick: ()=>actions.toggleMenu(node),
@@ -20613,13 +20795,18 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20613
20795
  warning
20614
20796
  ]
20615
20797
  });
20616
- } else content = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
20617
- className: "gramene-filter-text",
20618
- onClick: ()=>actions.toggleMenu(node),
20798
+ } else content = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
20619
20799
  children: [
20620
- node.category,
20621
- " |\xa0",
20622
- node.name
20800
+ expBadge,
20801
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
20802
+ className: "gramene-filter-text",
20803
+ onClick: ()=>actions.toggleMenu(node),
20804
+ children: [
20805
+ node.category,
20806
+ " |\xa0",
20807
+ node.name
20808
+ ]
20809
+ })
20623
20810
  ]
20624
20811
  });
20625
20812
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
@@ -20637,6 +20824,8 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
20637
20824
  negate: props.doNegateGrameneFilter,
20638
20825
  deleteNode: props.doDeleteGrameneFilter,
20639
20826
  changeOperation: props.doChangeGrameneFilterOperation,
20827
+ expand: props.doExpandGrameneFilter,
20828
+ removeExpansion: props.doRemoveGrameneExpansion,
20640
20829
  selectTarget: props.doMoveOrCopyGrameneFilter,
20641
20830
  markTargets: props.doMarkGrameneFilterTargets,
20642
20831
  unmarkTargets: props.doUnmarkGrameneFilterTargets,
@@ -20677,7 +20866,7 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
20677
20866
  })
20678
20867
  });
20679
20868
  };
20680
- const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
20869
+ const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
20681
20870
  const $693dd8c7a5607c3a$var$ResultsCmp = (props)=>{
20682
20871
  let activeViews = props.grameneViews.options;
20683
20872
  return props.grameneFilters.rightIdx > 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {