gramene-search 2.15.7 → 2.16.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,38 @@ 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
+ };
869
+ function $af4441dd29af05df$export$a6dc3fef1cecdf28(node) {
870
+ const type = node && node.expand;
871
+ return typeof type === 'string' && type ? type : null;
872
+ }
873
+ function $af4441dd29af05df$export$c8b6102be216752a(token) {
874
+ return String(token).replace(/[^A-Za-z0-9_-]/g, '-');
875
+ }
876
+ function $af4441dd29af05df$export$fb4df7b076406c76(subquery) {
877
+ return String(subquery).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
878
+ }
879
+ function $af4441dd29af05df$export$90cfa1f3fbcb6430(node) {
880
+ const type = $af4441dd29af05df$export$a6dc3fef1cecdf28(node);
881
+ if (!type) return null;
882
+ const spec = $af4441dd29af05df$export$da3188ffd2da2525[type];
883
+ return `EXPAND: ${spec ? spec.label : type}`;
884
+ }
853
885
  function $af4441dd29af05df$var$reindexTree(node, idx) {
854
886
  node.leftIdx = idx;
855
887
  idx++;
@@ -988,11 +1020,33 @@ const $af4441dd29af05df$var$grameneFilters = {
988
1020
  searchOffset: 0
989
1021
  });
990
1022
  let node = $af4441dd29af05df$var$findNodeWithLeftIdx(newState, payload.leftIdx);
991
- node.operation = node.operation === 'AND' ? 'OR' : 'AND';
1023
+ // Set an explicit operation when one is supplied; otherwise keep the
1024
+ // historical AND<->OR flip for callers that just pass the node.
1025
+ node.operation = payload.newOperation ? payload.newOperation : node.operation === 'AND' ? 'OR' : 'AND';
992
1026
  $af4441dd29af05df$var$markSubtree(newState, node, false);
993
1027
  return newState;
994
1028
  }
995
1029
  break;
1030
+ case 'GRAMENE_FILTER_EXPANSION_SET':
1031
+ {
1032
+ // Setting or clearing an expansion is just a field on the node — the tree
1033
+ // shape is untouched, so there is nothing to reindex and no special case
1034
+ // for the root. `payload.expand` of null/undefined removes it.
1035
+ newState = Object.assign({}, state, {
1036
+ status: 'search',
1037
+ showMarked: true,
1038
+ searchOffset: 0
1039
+ });
1040
+ let node = $af4441dd29af05df$var$findNodeWithLeftIdx(newState, payload.leftIdx);
1041
+ if (!node) break;
1042
+ // Leave showMenu alone, as GRAMENE_FILTER_NEGATED does. The radios are a
1043
+ // setting rather than a command, so the menu stays open and the selection
1044
+ // updates in place — you can switch type or clear without reopening it.
1045
+ if (payload.expand) node.expand = payload.expand;
1046
+ else delete node.expand;
1047
+ $af4441dd29af05df$var$markSubtree(newState, node, false);
1048
+ return newState;
1049
+ }
996
1050
  case 'GRAMENE_FILTER_MOVED':
997
1051
  {
998
1052
  newState = Object.assign({}, state, {
@@ -1163,6 +1217,48 @@ const $af4441dd29af05df$var$grameneFilters = {
1163
1217
  ]
1164
1218
  });
1165
1219
  },
1220
+ // Expand a node (leaf or group) along a relationship, e.g. "orthologs of this".
1221
+ doExpandGrameneFilter: (filter, type)=>({ dispatch: dispatch })=>{
1222
+ dispatch({
1223
+ type: 'BATCH_ACTIONS',
1224
+ actions: [
1225
+ {
1226
+ type: 'GRAMENE_SEARCH_CLEARED'
1227
+ },
1228
+ {
1229
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1230
+ },
1231
+ {
1232
+ type: 'GRAMENE_FILTER_EXPANSION_SET',
1233
+ payload: {
1234
+ leftIdx: filter.leftIdx,
1235
+ expand: type
1236
+ }
1237
+ }
1238
+ ]
1239
+ });
1240
+ },
1241
+ // Clear a node's expansion, leaving the node and its filters in place.
1242
+ doRemoveGrameneExpansion: (filter)=>({ dispatch: dispatch })=>{
1243
+ dispatch({
1244
+ type: 'BATCH_ACTIONS',
1245
+ actions: [
1246
+ {
1247
+ type: 'GRAMENE_SEARCH_CLEARED'
1248
+ },
1249
+ {
1250
+ type: 'GRAMENE_TAXONOMY_CLEARED'
1251
+ },
1252
+ {
1253
+ type: 'GRAMENE_FILTER_EXPANSION_SET',
1254
+ payload: {
1255
+ leftIdx: filter.leftIdx,
1256
+ expand: null
1257
+ }
1258
+ }
1259
+ ]
1260
+ });
1261
+ },
1166
1262
  doMoveOrCopyGrameneFilter: (target)=>({ dispatch: dispatch, getState: getState })=>{
1167
1263
  const state = getState();
1168
1264
  if (state.grameneFilters.moveCopyMode) {
@@ -1301,18 +1397,45 @@ const $af4441dd29af05df$var$grameneFilters = {
1301
1397
  const isQuery = new RegExp(/\([a-zA-Z0-9_]+:[a-zA-Z0-9_]+\s/);
1302
1398
  const isRegionQuery = new RegExp(/^\(map:/);
1303
1399
  const idWithColon = new RegExp(/^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$/);
1400
+ // The node's own clause, before negation and before any expansion.
1401
+ function ownQuery(node) {
1402
+ if (node.hasOwnProperty('children')) {
1403
+ // Only AND and OR are valid infix operators. Anything else — a token from a
1404
+ // newer client arriving via ?filters= or a saved view — would otherwise be
1405
+ // interpolated raw and produce a Solr 400, which surfaces as blank results
1406
+ // across every view rather than as an error.
1407
+ const op = node.operation === 'AND' || node.operation === 'OR' ? node.operation : 'OR';
1408
+ const kids = Array.isArray(node.children) ? node.children : [];
1409
+ return `(${kids.map((c)=>getQuery(c)).sort().join(` ${op} `)})`;
1410
+ }
1411
+ // this node is a suggestion
1412
+ if (node.fq_field === 'location' && isRegionQuery.test(node.fq_value)) return `${node.fq_value}`;
1413
+ if (isQuery.test(node.fq_value)) return `${node.fq_value.replace(/:/g, '\\:')}`;
1414
+ if (hasSpaces.test(node.fq_value)) return `${node.fq_field}:"${node.fq_value}"`;
1415
+ else if (idWithColon.test(node.fq_value)) return `${node.fq_field}:${node.fq_value.replace(/:/g, '\\:')}`;
1416
+ else return `${node.fq_field}:${node.fq_value}`;
1417
+ }
1304
1418
  function getQuery(node) {
1305
1419
  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
- }
1420
+ const clause = ownQuery(node);
1421
+ const expType = $af4441dd29af05df$export$a6dc3fef1cecdf28(node);
1422
+ // No expansion on this node: unchanged from before.
1423
+ if (!expType) return `${negate}${clause}`;
1424
+ const spec = $af4441dd29af05df$export$da3188ffd2da2525[expType];
1425
+ // Unknown expansion type — e.g. a link shared from a newer client. Degrade to
1426
+ // the un-expanded clause rather than emitting something broken.
1427
+ if (!spec) return `${negate}${clause}`;
1428
+ // An expansion of nothing must match nothing; '*:*' here would turn an empty
1429
+ // group into "every gene in the index".
1430
+ if (!clause || clause === '()') return `${negate}(*:* AND NOT *:*)`;
1431
+ // The braces MUST stay inside a quoted `_query_` value. A bare `{!graph ...}`
1432
+ // in `q` is unreliable and fails *silently* — observed returning 0 rows in
1433
+ // some shapes and millions in others, always HTTP 200, depending on the seed
1434
+ // and the endpoint in front of Solr. Don't factor `graph` out to module scope
1435
+ // where it could be used somewhere that isn't wrapped this way.
1436
+ const graph = `{!graph from=${spec.from} to=${spec.to} maxDepth=1 returnRoot=true}`;
1437
+ // Negation applies to the expanded set: "NOT an ortholog of msd2".
1438
+ return `${negate}(_query_:"${graph}${$af4441dd29af05df$export$fb4df7b076406c76(clause)}")`;
1316
1439
  }
1317
1440
  if (state.grameneFilters.rightIdx === 1) return '*:*';
1318
1441
  return `*:* AND (${getQuery(state.grameneFilters)})`;
@@ -6270,6 +6393,7 @@ const $472fe3745b238881$var$FILTER_NODE_KEEP = [
6270
6393
  'rightIdx',
6271
6394
  'operation',
6272
6395
  'negate',
6396
+ 'expand',
6273
6397
  'fq_field',
6274
6398
  'fq_value',
6275
6399
  'name',
@@ -6279,6 +6403,7 @@ const $472fe3745b238881$var$FILTER_NODE_KEEP = [
6279
6403
  const $472fe3745b238881$var$FILTER_ROOT_KEEP = [
6280
6404
  'operation',
6281
6405
  'negate',
6406
+ 'expand',
6282
6407
  'leftIdx',
6283
6408
  'rightIdx',
6284
6409
  'children',
@@ -20063,6 +20188,7 @@ var $7d7b7dad5e71dfd3$export$2e2bcd8739ae039 = $7d7b7dad5e71dfd3$var$Facets;
20063
20188
 
20064
20189
 
20065
20190
 
20191
+
20066
20192
  // "Save this view" UI — sits inside the Auth sidebar panel.
20067
20193
  //
20068
20194
  // Sign-in-gated by Auth.js (we only mount this when `user` is truthy). Opens
@@ -20506,7 +20632,10 @@ function $693dd8c7a5607c3a$var$logAction(action) {
20506
20632
  }
20507
20633
  const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode, showMarked: showMarked, actions: actions })=>{
20508
20634
  let classes = 'gramene-filter gramene-filter';
20509
- if (node.operation) classes = `${classes}-${node.operation}`;
20635
+ if (node.operation) classes = `${classes}-${(0, $af4441dd29af05df$export$c8b6102be216752a)(node.operation)}`;
20636
+ // An expansion decorates whatever node it sits on, leaf or group — same as negate.
20637
+ const expType = (0, $af4441dd29af05df$export$a6dc3fef1cecdf28)(node);
20638
+ if (expType) classes = `${classes} gramene-filter-EXPAND gramene-filter-EXPAND-${(0, $af4441dd29af05df$export$c8b6102be216752a)(expType)}`;
20510
20639
  if (node.negate) classes = `${classes} gramene-filter-NOT`;
20511
20640
  if (showMarked && node.marked) classes = `${classes} gramene-filter-marked`;
20512
20641
  let content = '';
@@ -20527,6 +20656,40 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20527
20656
  })
20528
20657
  ]
20529
20658
  }, key++));
20659
+ // A node carries at most one expansion, so the types are radios rather than
20660
+ // separate commands. Any node can carry one, leaf or group — attaching it to a
20661
+ // group means "expand whatever this group matches".
20662
+ menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
20663
+ className: "gramene-filter-menu-label",
20664
+ children: "expand filter(s)"
20665
+ }, key++));
20666
+ Object.keys((0, $af4441dd29af05df$export$da3188ffd2da2525)).forEach((type)=>{
20667
+ const checked = expType === type;
20668
+ menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
20669
+ className: "gramene-filter-menu-radio",
20670
+ onClick: ()=>{
20671
+ // Re-picking the active type clears it — the only way to remove an
20672
+ // expansion from the root, which has no `delete`.
20673
+ if (checked) {
20674
+ $693dd8c7a5607c3a$var$logAction(`Remove expansion`);
20675
+ actions.removeExpansion(node);
20676
+ } else {
20677
+ $693dd8c7a5607c3a$var$logAction(`Expand by ${type}`);
20678
+ actions.expand(node, type);
20679
+ }
20680
+ },
20681
+ children: [
20682
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
20683
+ type: "radio",
20684
+ readOnly: true,
20685
+ checked: checked,
20686
+ onChange: ()=>{},
20687
+ name: `gramene-expand-${node.leftIdx}`
20688
+ }),
20689
+ (0, $af4441dd29af05df$export$da3188ffd2da2525)[type].label
20690
+ ]
20691
+ }, key++));
20692
+ });
20530
20693
  if (node.leftIdx > 0) {
20531
20694
  menuItems.push(/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("li", {
20532
20695
  onClick: ()=>{
@@ -20596,6 +20759,13 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20596
20759
  })
20597
20760
  });
20598
20761
  }
20762
+ // Shown above the node's own label, since the expansion applies to whatever that
20763
+ // node resolves to rather than replacing it.
20764
+ const expBadge = expType ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
20765
+ className: "gramene-filter-expansion",
20766
+ onClick: ()=>actions.toggleMenu(node),
20767
+ children: (0, $af4441dd29af05df$export$90cfa1f3fbcb6430)(node)
20768
+ }) : null;
20599
20769
  if (node.operation) {
20600
20770
  children = node.children.map((child, idx)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($693dd8c7a5607c3a$var$Filter, {
20601
20771
  moveCopyMode: moveCopyMode,
@@ -20605,6 +20775,7 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20605
20775
  }, idx));
20606
20776
  content = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
20607
20777
  children: [
20778
+ expBadge,
20608
20779
  /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
20609
20780
  className: "gramene-filter-operation",
20610
20781
  onClick: ()=>actions.toggleMenu(node),
@@ -20613,13 +20784,18 @@ const $693dd8c7a5607c3a$var$Filter = ({ node: node, moveCopyMode: moveCopyMode,
20613
20784
  warning
20614
20785
  ]
20615
20786
  });
20616
- } else content = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
20617
- className: "gramene-filter-text",
20618
- onClick: ()=>actions.toggleMenu(node),
20787
+ } else content = /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactjsxruntime.Fragment), {
20619
20788
  children: [
20620
- node.category,
20621
- " |\xa0",
20622
- node.name
20789
+ expBadge,
20790
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
20791
+ className: "gramene-filter-text",
20792
+ onClick: ()=>actions.toggleMenu(node),
20793
+ children: [
20794
+ node.category,
20795
+ " |\xa0",
20796
+ node.name
20797
+ ]
20798
+ })
20623
20799
  ]
20624
20800
  });
20625
20801
  return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
@@ -20637,6 +20813,8 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
20637
20813
  negate: props.doNegateGrameneFilter,
20638
20814
  deleteNode: props.doDeleteGrameneFilter,
20639
20815
  changeOperation: props.doChangeGrameneFilterOperation,
20816
+ expand: props.doExpandGrameneFilter,
20817
+ removeExpansion: props.doRemoveGrameneExpansion,
20640
20818
  selectTarget: props.doMoveOrCopyGrameneFilter,
20641
20819
  markTargets: props.doMarkGrameneFilterTargets,
20642
20820
  unmarkTargets: props.doUnmarkGrameneFilterTargets,
@@ -20677,7 +20855,7 @@ const $693dd8c7a5607c3a$var$FiltersCmp = (props)=>{
20677
20855
  })
20678
20856
  });
20679
20857
  };
20680
- const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
20858
+ const $693dd8c7a5607c3a$export$92a576f896a9bf72 = (0, $gXNCa$reduxbundlerreact.connect)('selectGrameneFilters', 'selectConfiguration', 'doNegateGrameneFilter', 'doDeleteGrameneFilter', 'doChangeGrameneFilterOperation', 'doExpandGrameneFilter', 'doRemoveGrameneExpansion', 'doMoveOrCopyGrameneFilter', 'doMarkGrameneFilterTargets', 'doUnmarkGrameneFilterTargets', 'doToggleGrameneFilterMenu', 'doClearGrameneFilters', $693dd8c7a5607c3a$var$FiltersCmp);
20681
20859
  const $693dd8c7a5607c3a$var$ResultsCmp = (props)=>{
20682
20860
  let activeViews = props.grameneViews.options;
20683
20861
  return props.grameneFilters.rightIdx > 0 ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {