jbrowse-plugin-msaview 5.15.0 → 5.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.
Files changed (49) hide show
  1. package/README.md +2 -0
  2. package/dist/MsaViewPanel/afterCreateAutoruns.d.ts +18 -0
  3. package/dist/MsaViewPanel/afterCreateAutoruns.js +25 -3
  4. package/dist/MsaViewPanel/loadProteinDomains.d.ts +5 -1
  5. package/dist/MsaViewPanel/loadProteinDomains.js +16 -4
  6. package/dist/MsaViewPanel/loadProteinDomains.test.js +36 -5
  7. package/dist/MsaViewPanel/model.d.ts +158 -6
  8. package/dist/MsaViewPanel/model.js +162 -8
  9. package/dist/MsaViewPanel/resolveConnectedTranscript.d.ts +32 -3
  10. package/dist/MsaViewPanel/resolveConnectedTranscript.js +70 -22
  11. package/dist/MsaViewPanel/resolveConnectedTranscript.test.js +162 -5
  12. package/dist/MsaViewPanel/transcriptMap.d.ts +17 -1
  13. package/dist/MsaViewPanel/transcriptMap.js +13 -13
  14. package/dist/MsaViewPanel/transcriptMap.test.js +20 -34
  15. package/dist/MsaViewPanel/util.d.ts +3 -0
  16. package/dist/MsaViewPanel/util.js +7 -0
  17. package/dist/MsaViewPanel/viewReport.d.ts +113 -0
  18. package/dist/MsaViewPanel/viewReport.js +124 -0
  19. package/dist/MsaViewPanel/viewState.test.d.ts +1 -0
  20. package/dist/MsaViewPanel/viewState.test.js +303 -0
  21. package/dist/chunks/chunk-JUE34WZU.js +2 -0
  22. package/dist/chunks/chunk-JUE34WZU.js.map +7 -0
  23. package/dist/chunks/model-DAIL6BNQ.js +12 -0
  24. package/dist/chunks/{model-JKWKYCBL.js.map → model-DAIL6BNQ.js.map} +4 -4
  25. package/dist/jbrowse-plugin-msaview.esm.js +2 -2
  26. package/dist/jbrowse-plugin-msaview.esm.js.map +1 -1
  27. package/dist/utils/ncbiDomains.d.ts +14 -1
  28. package/dist/utils/ncbiDomains.js +23 -7
  29. package/dist/utils/ncbiDomains.test.js +54 -5
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/package.json +1 -1
  33. package/src/MsaViewPanel/afterCreateAutoruns.ts +39 -3
  34. package/src/MsaViewPanel/loadProteinDomains.test.ts +42 -7
  35. package/src/MsaViewPanel/loadProteinDomains.ts +16 -9
  36. package/src/MsaViewPanel/model.ts +189 -9
  37. package/src/MsaViewPanel/resolveConnectedTranscript.test.ts +198 -5
  38. package/src/MsaViewPanel/resolveConnectedTranscript.ts +89 -23
  39. package/src/MsaViewPanel/transcriptMap.test.ts +21 -38
  40. package/src/MsaViewPanel/transcriptMap.ts +15 -17
  41. package/src/MsaViewPanel/util.ts +9 -0
  42. package/src/MsaViewPanel/viewReport.ts +185 -0
  43. package/src/MsaViewPanel/viewState.test.ts +340 -0
  44. package/src/utils/ncbiDomains.test.ts +62 -5
  45. package/src/utils/ncbiDomains.ts +32 -8
  46. package/src/version.ts +1 -1
  47. package/dist/chunks/chunk-MXZ7GONI.js +0 -2
  48. package/dist/chunks/chunk-MXZ7GONI.js.map +0 -7
  49. package/dist/chunks/model-JKWKYCBL.js +0 -12
package/README.md CHANGED
@@ -79,6 +79,8 @@ Linking and embedding:
79
79
  - [Launch parameters](docs/launch-parameters.md): every argument and field.
80
80
  - [Linked views](docs/linked-views.md): how a column reaches its codon and a
81
81
  protein3d structure.
82
+ - [Reading the view](docs/reading-the-view.md): `view.report` and `notices`,
83
+ what a launch resolved to, as data a script or an agent reads.
82
84
 
83
85
  Working on the plugin:
84
86
 
@@ -8,6 +8,18 @@ export declare function loadStoredData(self: JBrowsePluginMsaViewModel): void;
8
8
  * refusing IndexedDB fails once rather than in a loop.
9
9
  */
10
10
  export declare function storeDataToIndexedDB(self: JBrowsePluginMsaViewModel): void;
11
+ interface TranscriptWait {
12
+ connectedTranscript?: string;
13
+ connectedFeature?: unknown;
14
+ blastParams?: unknown;
15
+ orthologParams?: unknown;
16
+ launchCompleted: boolean;
17
+ }
18
+ /**
19
+ * A launch that has not run yet and is held back by the transcript lookup, so
20
+ * a lookup that fails is a launch that cannot proceed.
21
+ */
22
+ export declare function launchAwaitsTranscript(self: TranscriptWait): boolean;
11
23
  export declare function launchOrthologsIfNeeded(self: JBrowsePluginMsaViewModel): void;
12
24
  export declare function launchBlastIfNeeded(self: JBrowsePluginMsaViewModel): void;
13
25
  /**
@@ -57,4 +69,10 @@ export declare function syncGenomeHoverToMsaColumn(self: JBrowsePluginMsaViewMod
57
69
  */
58
70
  export declare function observeProteinHighlights(self: JBrowsePluginMsaViewModel): () => void;
59
71
  export declare function runCleanup(): void;
72
+ /**
73
+ * Waits for the genome view to show an assembly, since the track is added to
74
+ * that assembly: before navigation `showDomainsOnGenome` has none to name, and
75
+ * clearing the request then dropped it unfulfilled.
76
+ */
60
77
  export declare function showDomainsOnGenomeIfAsked(self: JBrowsePluginMsaViewModel): void;
78
+ export {};
@@ -11,7 +11,7 @@ import { cleanupOldData, generateDataStoreId, retrieveMsaData, storeMsaData, } f
11
11
  import { runLaunch } from './runLaunch';
12
12
  import { getProteinViews } from './structureConnection';
13
13
  import { proteinPositionsInRange } from './transcriptMap';
14
- import { getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
14
+ import { errorMessage, getUniprotIdFromAlphaFoldUrl, hasQueryRow, transcriptPosToVisibleCol, } from './util';
15
15
  const EXPIRED_MESSAGE = "This view's alignment is no longer in browser storage. Stored alignments are kept for 7 days after they were last used, and are lost when site data is cleared.";
16
16
  const RELAUNCHABLE = ' Retry runs the original search again and rebuilds it.';
17
17
  const START_OVER = ' Relaunch it from the gene to rebuild it.';
@@ -140,6 +140,15 @@ export function storeDataToIndexedDB(self) {
140
140
  function awaitingTranscript(self) {
141
141
  return !!self.connectedTranscript && !self.connectedFeature;
142
142
  }
143
+ /**
144
+ * A launch that has not run yet and is held back by the transcript lookup, so
145
+ * a lookup that fails is a launch that cannot proceed.
146
+ */
147
+ export function launchAwaitsTranscript(self) {
148
+ return (!!(self.blastParams ?? self.orthologParams) &&
149
+ !self.launchCompleted &&
150
+ awaitingTranscript(self));
151
+ }
143
152
  /**
144
153
  * The params are the request and `launchCompleted` marks it done. They stay in
145
154
  * place on failure, so the error is attributable, and on success, because a
@@ -180,22 +189,30 @@ export function autoLoadProteinDomains(self) {
180
189
  };
181
190
  addDisposer(self, abort);
182
191
  self.setDomainsRequested(true);
192
+ self.setLoadingDomains(true);
183
193
  self.setStatus({
184
194
  msg: 'Fetching protein domains from NCBI',
185
195
  onCancel: abort,
186
196
  });
187
197
  void (async () => {
188
198
  try {
189
- await loadProteinDomains(self, controller.signal);
199
+ const notice = await loadProteinDomains(self, controller.signal);
200
+ if (notice && isAlive(self)) {
201
+ self.addWarning(notice);
202
+ }
190
203
  }
191
204
  catch (e) {
192
205
  if (!isAbortError(e)) {
193
206
  console.error('[msaview-domains] auto-load failed:', e);
207
+ if (isAlive(self)) {
208
+ self.addWarning(`Protein domains did not load from NCBI: ${errorMessage(e)}`);
209
+ }
194
210
  }
195
211
  }
196
212
  finally {
197
213
  if (isAlive(self)) {
198
214
  self.setStatus(undefined);
215
+ self.setLoadingDomains(false);
199
216
  }
200
217
  }
201
218
  })();
@@ -390,9 +407,14 @@ export function runCleanup() {
390
407
  console.error('Failed to cleanup old MSA data:', e);
391
408
  });
392
409
  }
410
+ /**
411
+ * Waits for the genome view to show an assembly, since the track is added to
412
+ * that assembly: before navigation `showDomainsOnGenome` has none to name, and
413
+ * clearing the request then dropped it unfulfilled.
414
+ */
393
415
  export function showDomainsOnGenomeIfAsked(self) {
394
416
  if (self.domainsOnGenome &&
395
- self.connectedView &&
417
+ self.connectedView?.assemblyNames[0] &&
396
418
  self.connectedDomains.features.length > 0) {
397
419
  self.showDomainsOnGenome();
398
420
  self.setDomainsOnGenome(false);
@@ -8,6 +8,10 @@ interface DomainModel {
8
8
  * annotations. Each launch stores a row's accession in its row data, so those
9
9
  * are looked up via efetch and keyed by MSA row name, which is what
10
10
  * react-msaview matches annotations against.
11
+ *
12
+ * Throws when NCBI answered for none of the rows. When it answered for some,
13
+ * their domains are drawn and the returned notice names what is missing. A
14
+ * set of proteins with no CDD annotation leaves the overlay as it was.
11
15
  */
12
- export declare function loadProteinDomains(self: DomainModel, signal?: AbortSignal): Promise<void>;
16
+ export declare function loadProteinDomains(self: DomainModel, signal?: AbortSignal): Promise<string | undefined>;
13
17
  export {};
@@ -1,16 +1,26 @@
1
1
  import { fetchProteinDomains } from '../utils/ncbiDomains';
2
+ import { errorMessage } from './util';
2
3
  /**
3
4
  * Overlay protein domains on the alignment using NCBI's pre-computed CDD
4
5
  * annotations. Each launch stores a row's accession in its row data, so those
5
6
  * are looked up via efetch and keyed by MSA row name, which is what
6
7
  * react-msaview matches annotations against.
8
+ *
9
+ * Throws when NCBI answered for none of the rows. When it answered for some,
10
+ * their domains are drawn and the returned notice names what is missing. A
11
+ * set of proteins with no CDD annotation leaves the overlay as it was.
7
12
  */
8
13
  export async function loadProteinDomains(self, signal) {
9
14
  const rowAccessions = Object.entries(self.rowData).flatMap(([rowName, row]) => row?.Accession ? [{ rowName, accession: row.Accession }] : []);
10
15
  if (rowAccessions.length === 0) {
11
16
  throw new Error('No NCBI accessions found in alignment rows');
12
17
  }
13
- const byAccession = await fetchProteinDomains(rowAccessions.map(r => r.accession), signal);
18
+ const asked = new Set(rowAccessions.map(r => r.accession));
19
+ const { byAccession, failure } = await fetchProteinDomains([...asked], signal);
20
+ const answered = [...asked].filter(a => byAccession.has(a)).length;
21
+ if (failure !== undefined && answered === 0) {
22
+ throw new Error(errorMessage(failure), { cause: failure });
23
+ }
14
24
  const annotations = rowAccessions.flatMap(({ rowName, accession }) => (byAccession.get(accession) ?? []).flatMap(({ signature, locations, featureType }) => signature.entry
15
25
  ? locations.map(({ start, end }) => ({
16
26
  id: rowName,
@@ -22,8 +32,10 @@ export async function loadProteinDomains(self, signal) {
22
32
  end,
23
33
  }))
24
34
  : []));
25
- if (annotations.length === 0) {
26
- throw new Error('No CDD domain annotations found for these proteins');
35
+ if (annotations.length > 0) {
36
+ self.setAnnotations(annotations);
27
37
  }
28
- self.setAnnotations(annotations);
38
+ return failure === undefined
39
+ ? undefined
40
+ : `Protein domains loaded for ${answered} of ${asked.size} proteins; NCBI failed on the rest: ${errorMessage(failure)}`;
29
41
  }
@@ -16,12 +16,15 @@ function model(treeMetadata) {
16
16
  };
17
17
  }
18
18
  test('an accession row becomes one annotation per location, keyed by row name', async () => {
19
- vi.mocked(fetchProteinDomains).mockResolvedValue(new Map([['NP_1', [MATCH]]]));
19
+ vi.mocked(fetchProteinDomains).mockResolvedValue({
20
+ byAccession: new Map([['NP_1', [MATCH]]]),
21
+ failure: undefined,
22
+ });
20
23
  const self = model({
21
24
  QUERY: { Accession: 'NP_1' },
22
25
  other: { 'Scientific name': 'Mus musculus' },
23
26
  });
24
- await loadProteinDomains(self);
27
+ expect(await loadProteinDomains(self)).toBeUndefined();
25
28
  expect(self.setAnnotations).toHaveBeenCalledWith([
26
29
  {
27
30
  id: 'QUERY',
@@ -34,9 +37,37 @@ test('an accession row becomes one annotation per location, keyed by row name',
34
37
  },
35
38
  ]);
36
39
  });
37
- test('says so rather than clearing the overlay when NCBI has nothing', async () => {
38
- vi.mocked(fetchProteinDomains).mockResolvedValue(new Map());
40
+ test('proteins with no CDD annotation leave the overlay alone', async () => {
41
+ vi.mocked(fetchProteinDomains).mockResolvedValue({
42
+ byAccession: new Map([['NP_1', []]]),
43
+ failure: undefined,
44
+ });
45
+ const self = model({ QUERY: { Accession: 'NP_1' } });
46
+ expect(await loadProteinDomains(self)).toBeUndefined();
47
+ expect(self.setAnnotations).not.toHaveBeenCalled();
48
+ });
49
+ test('a fetch that failed outright throws its failure', async () => {
50
+ const failure = new Error('HTTP 429 fetching efetch');
51
+ vi.mocked(fetchProteinDomains).mockResolvedValue({
52
+ byAccession: new Map(),
53
+ failure,
54
+ });
39
55
  const self = model({ QUERY: { Accession: 'NP_1' } });
40
- await expect(loadProteinDomains(self)).rejects.toThrow('No CDD domain annotations found');
56
+ await expect(loadProteinDomains(self)).rejects.toMatchObject({
57
+ message: 'HTTP 429 fetching efetch',
58
+ cause: failure,
59
+ });
41
60
  expect(self.setAnnotations).not.toHaveBeenCalled();
42
61
  });
62
+ test('a fetch that half failed draws what loaded and names what did not', async () => {
63
+ vi.mocked(fetchProteinDomains).mockResolvedValue({
64
+ byAccession: new Map([['NP_1', [MATCH]]]),
65
+ failure: new Error('HTTP 429 fetching efetch'),
66
+ });
67
+ const self = model({
68
+ QUERY: { Accession: 'NP_1' },
69
+ mouse: { Accession: 'NP_2' },
70
+ });
71
+ expect(await loadProteinDomains(self)).toBe('Protein domains loaded for 1 of 2 proteins; NCBI failed on the rest: HTTP 429 fetching efetch');
72
+ expect(self.setAnnotations).toHaveBeenCalledTimes(1);
73
+ });
@@ -6,10 +6,10 @@ import type { MsaDataPayload } from './msaDataStore';
6
6
  import type { MafRegion, MsaViewInitState } from './types';
7
7
  import type { TranscriptRef } from './util';
8
8
  import type { MenuItem } from '@jbrowse/core/ui';
9
+ import type { AbstractViewModel } from '@jbrowse/core/util';
9
10
  import type { Instance } from '@jbrowse/mobx-state-tree';
10
11
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view';
11
12
  type LGV = LinearGenomeViewModel;
12
- type MaybeLGV = LGV | undefined;
13
13
  export interface IRegion {
14
14
  refName: string;
15
15
  start: number;
@@ -524,7 +524,7 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
524
524
  readonly effectiveBodyMounted: boolean;
525
525
  menuItems(): MenuItem[];
526
526
  readonly ownTracks: import("@jbrowse/core/util").AbstractTrackModel[];
527
- readonly ownViews: import("@jbrowse/core/util").AbstractViewModel[];
527
+ readonly ownViews: AbstractViewModel[];
528
528
  } & {
529
529
  setDisplayName(name: string): void;
530
530
  setWidth(newWidth: number): void;
@@ -977,15 +977,48 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
977
977
  ownsDataStoreRow: boolean;
978
978
  launchController: AbortController | undefined;
979
979
  domainsRequested: boolean;
980
+ loadingDomains: boolean;
981
+ connectionError: unknown;
980
982
  } & {
981
983
  /**
982
984
  * #getter
985
+ * the linked transcript's genome mapping, or why it has none
983
986
  */
984
- readonly transcriptToMsaMap: import("./transcriptMap").TranscriptMap | undefined;
987
+ readonly transcriptMapping: {
988
+ reason: string;
989
+ } | {
990
+ map: {
991
+ codingPositions: number[];
992
+ g2p: Record<number, number>;
993
+ p2g: Record<number, number>;
994
+ p2gCodon: Record<number, number[]>;
995
+ refName: string;
996
+ strand: number;
997
+ };
998
+ } | undefined;
999
+ /**
1000
+ * #getter
1001
+ * whatever view `connectedViewId` names, genome view or not
1002
+ */
1003
+ readonly viewNamedByConnectedViewId: AbstractViewModel | undefined;
1004
+ } & {
985
1005
  /**
986
1006
  * #getter
1007
+ * the view `connectedViewId` names, when it is a genome view
987
1008
  */
988
- readonly connectedView: MaybeLGV;
1009
+ readonly connectedView: LGV | undefined;
1010
+ } & {
1011
+ /**
1012
+ * #getter
1013
+ */
1014
+ readonly transcriptToMsaMap: {
1015
+ codingPositions: number[];
1016
+ g2p: Record<number, number>;
1017
+ p2g: Record<number, number>;
1018
+ p2gCodon: Record<number, number[]>;
1019
+ refName: string;
1020
+ strand: number;
1021
+ } | undefined;
989
1022
  } & {
990
1023
  /**
991
1024
  * #getter
@@ -1035,11 +1068,108 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
1035
1068
  readonly unsavedDocuments: MsaDataPayload;
1036
1069
  /**
1037
1070
  * #getter
1038
- * true while a launch, stored data or a file is still loading and no
1039
- * error has ended it. jb.waitReady, the app's ready marker and
1071
+ * what is wrong with the link to the genome, derived from the view's
1072
+ * state so each clears when its cause does. A connection failure that
1073
+ * is already the view's `error` is not repeated here.
1074
+ */
1075
+ readonly linkNotices: string[];
1076
+ /**
1077
+ * #getter
1078
+ * true while a launch, stored data, a file, the transcript lookup or the
1079
+ * protein domains are still loading and no error has ended it. Every
1080
+ * term settles on failure. jb.waitReady, the app's ready marker and
1040
1081
  * screenshot tools read it for a view.
1041
1082
  */
1042
1083
  readonly showLoading: boolean;
1084
+ } & {
1085
+ /**
1086
+ * #getter
1087
+ * overrides base
1088
+ *
1089
+ * react-msaview's load and data warnings, plus the plugin's link
1090
+ * notices, which the header's warning chip lists and dismisses alike
1091
+ */
1092
+ readonly warnings: string[];
1093
+ } & {
1094
+ /**
1095
+ * #getter
1096
+ * what failed and left the view working: the messages the header's
1097
+ * warning chip lists. A notice does not end `showLoading`.
1098
+ */
1099
+ readonly notices: string[];
1100
+ } & {
1101
+ /**
1102
+ * #getter
1103
+ * what the view resolved to, as plain JSON. See docs/reading-the-view.md
1104
+ */
1105
+ readonly report: {
1106
+ loading: boolean;
1107
+ error: string | undefined;
1108
+ notices: string[];
1109
+ source: {
1110
+ kind: string;
1111
+ database: "uniprotkb_swissprot" | "uniprotkb" | "pan_proteomes" | "uniprotkb_trembl" | "swissprot" | "uniprotrefprot" | "rp75" | "rp55" | "rp35" | "rp15";
1112
+ msaAlgorithm: "clustalo" | "muscle" | "kalign" | "mafft" | "browser" | undefined;
1113
+ maxHits: number | undefined;
1114
+ accession: string | undefined;
1115
+ queryLength: number | undefined;
1116
+ jobId: string | undefined;
1117
+ completed: boolean;
1118
+ } | {
1119
+ kind: string;
1120
+ orthologSource: "ncbi" | "panther" | "uniref";
1121
+ taxId: number;
1122
+ identity: UnirefIdentity | undefined;
1123
+ maxSpecies: number | undefined;
1124
+ taxa: number[] | undefined;
1125
+ geneCandidates: string[];
1126
+ msaAlgorithm: "clustalo" | "muscle" | "kalign" | "mafft" | "browser";
1127
+ queryLength: number | undefined;
1128
+ jobId: string | undefined;
1129
+ completed: boolean;
1130
+ } | {
1131
+ kind: string;
1132
+ url: string;
1133
+ uri?: undefined;
1134
+ name?: undefined;
1135
+ location?: undefined;
1136
+ } | {
1137
+ kind: string;
1138
+ uri: string;
1139
+ name: string | undefined;
1140
+ url?: undefined;
1141
+ location?: undefined;
1142
+ } | {
1143
+ kind: string;
1144
+ location: string | undefined;
1145
+ url?: undefined;
1146
+ uri?: undefined;
1147
+ name?: undefined;
1148
+ } | {
1149
+ kind: string;
1150
+ url?: undefined;
1151
+ uri?: undefined;
1152
+ name?: undefined;
1153
+ location?: undefined;
1154
+ };
1155
+ rows: number;
1156
+ columns: number;
1157
+ domainAnnotations: number;
1158
+ uniprotId: string | undefined;
1159
+ queryRow: {
1160
+ name: string;
1161
+ found: boolean;
1162
+ offset: number;
1163
+ };
1164
+ connection: {
1165
+ viewId: string | undefined;
1166
+ viewOpen: boolean;
1167
+ transcript: string | undefined;
1168
+ refName: string | undefined;
1169
+ mapped: boolean;
1170
+ unmappedBecause: string | undefined;
1171
+ } | undefined;
1172
+ };
1043
1173
  } & {
1044
1174
  /**
1045
1175
  * #action
@@ -1130,6 +1260,21 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
1130
1260
  * #action
1131
1261
  */
1132
1262
  setDomainsRequested(arg: boolean): void;
1263
+ /**
1264
+ * #action
1265
+ */
1266
+ setLoadingDomains(arg: boolean): void;
1267
+ /**
1268
+ * #action
1269
+ * `connectedTranscript` could not be resolved. A launch waiting on it
1270
+ * cannot run, so for one that is the view's error; an alignment from
1271
+ * anywhere else is still worth showing, so then it is a notice.
1272
+ */
1273
+ failConnection(e: unknown): void;
1274
+ /**
1275
+ * #action
1276
+ */
1277
+ clearConnectionFailure(): void;
1133
1278
  /**
1134
1279
  * #action
1135
1280
  */
@@ -1163,6 +1308,13 @@ export default function stateModelFactory(): import("@jbrowse/mobx-state-tree").
1163
1308
  * once per view, never fired again.
1164
1309
  */
1165
1310
  reset(): void;
1311
+ } & {
1312
+ /**
1313
+ * #action
1314
+ * overrides base, so dismissing the warning chip hides the link
1315
+ * notices too until their cause changes
1316
+ */
1317
+ clearWarnings(): void;
1166
1318
  } & {
1167
1319
  /**
1168
1320
  * #method
@@ -3,14 +3,19 @@ import { getSession, isSessionWithAddSessionTrack, isSessionWithDeleteTrackConf,
3
3
  import { addDisposer, getSnapshot, types } from '@jbrowse/mobx-state-tree';
4
4
  import { autorun } from 'mobx';
5
5
  import { MSAModelF } from 'react-msaview';
6
- import { autoLoadProteinDomains, launchBlastIfNeeded, launchOrthologsIfNeeded, loadStoredData, observeProteinHighlights, processInit, runCleanup, storeDataToIndexedDB, showDomainsOnGenomeIfAsked, syncGenomeHoverToMsaColumn, } from './afterCreateAutoruns';
6
+ import { autoLoadProteinDomains, launchAwaitsTranscript, launchBlastIfNeeded, launchOrthologsIfNeeded, loadStoredData, observeProteinHighlights, processInit, runCleanup, storeDataToIndexedDB, showDomainsOnGenomeIfAsked, syncGenomeHoverToMsaColumn, } from './afterCreateAutoruns';
7
7
  import { connectedDomains } from './connectedDomains';
8
8
  import { domainTrackConf } from './domainTrack';
9
9
  import { isLaunching } from './isLaunching';
10
10
  import { msaCoordToGenomeCoord, msaCoordToGenomeRegions, } from './msaCoordToGenomeCoord';
11
11
  import { resolveConnectedTranscriptIfNeeded } from './resolveConnectedTranscript';
12
12
  import { syncSessionHighlights } from './sessionHighlights';
13
- import { transcriptMap } from './transcriptMap';
13
+ import { transcriptMapping } from './transcriptMap';
14
+ import { errorMessage, hasQueryRow } from './util';
15
+ import { viewReport } from './viewReport';
16
+ function isLinearGenomeView(view) {
17
+ return view.type === 'LinearGenomeView';
18
+ }
14
19
  /**
15
20
  * Where the homolog set comes from. NCBI's ortholog sets cover vertebrates and
16
21
  * insects; PANTHER's span its 144 reference proteomes, human to yeast to
@@ -163,23 +168,57 @@ export default function stateModelFactory() {
163
168
  * when NCBI returns no domains (leaving the annotation list empty)
164
169
  */
165
170
  domainsRequested: false,
171
+ /**
172
+ * #volatile
173
+ * the protein domain auto-load is in flight
174
+ */
175
+ loadingDomains: false,
176
+ /**
177
+ * #volatile
178
+ * why `connectedTranscript` could not be resolved: the lookup missed,
179
+ * or the genome view it needs is absent or has no feature tracks. It
180
+ * is also the view's `error` when a launch was waiting on the
181
+ * transcript, and a notice otherwise
182
+ */
183
+ connectionError: undefined,
166
184
  }))
167
185
  .views(self => ({
168
186
  /**
169
187
  * #getter
188
+ * the linked transcript's genome mapping, or why it has none
170
189
  */
171
- get transcriptToMsaMap() {
190
+ get transcriptMapping() {
172
191
  return self.connectedFeature
173
- ? transcriptMap(self.connectedFeature)
192
+ ? transcriptMapping(self.connectedFeature)
174
193
  : undefined;
175
194
  },
176
195
  /**
177
196
  * #getter
197
+ * whatever view `connectedViewId` names, genome view or not
178
198
  */
179
- get connectedView() {
199
+ get viewNamedByConnectedViewId() {
180
200
  const { views } = getSession(self);
181
201
  return views.find(f => f.id === self.connectedViewId);
182
202
  },
203
+ }))
204
+ .views(self => ({
205
+ /**
206
+ * #getter
207
+ * the view `connectedViewId` names, when it is a genome view
208
+ */
209
+ get connectedView() {
210
+ const view = self.viewNamedByConnectedViewId;
211
+ return view && isLinearGenomeView(view) ? view : undefined;
212
+ },
213
+ }))
214
+ .views(self => ({
215
+ /**
216
+ * #getter
217
+ */
218
+ get transcriptToMsaMap() {
219
+ const mapping = self.transcriptMapping;
220
+ return mapping && 'map' in mapping ? mapping.map : undefined;
221
+ },
183
222
  }))
184
223
  .views(self => ({
185
224
  /**
@@ -267,15 +306,84 @@ export default function stateModelFactory() {
267
306
  },
268
307
  /**
269
308
  * #getter
270
- * true while a launch, stored data or a file is still loading and no
271
- * error has ended it. jb.waitReady, the app's ready marker and
309
+ * what is wrong with the link to the genome, derived from the view's
310
+ * state so each clears when its cause does. A connection failure that
311
+ * is already the view's `error` is not repeated here.
312
+ */
313
+ get linkNotices() {
314
+ const { connectionError, error, transcriptMapping, connectedFeature, mafRegion, querySeqName, dataInitialized, } = self;
315
+ return [
316
+ ...(connectionError !== undefined && connectionError !== error
317
+ ? [errorMessage(connectionError)]
318
+ : []),
319
+ ...(transcriptMapping && 'reason' in transcriptMapping
320
+ ? [
321
+ `The linked transcript is not mapped to the genome: ${transcriptMapping.reason}`,
322
+ ]
323
+ : []),
324
+ ...((connectedFeature || mafRegion) &&
325
+ dataInitialized &&
326
+ !hasQueryRow(self)
327
+ ? [
328
+ `No row is named "${querySeqName}", the query row, so the alignment is not linked to the genome`,
329
+ ]
330
+ : []),
331
+ ];
332
+ },
333
+ /**
334
+ * #getter
335
+ * true while a launch, stored data, a file, the transcript lookup or the
336
+ * protein domains are still loading and no error has ended it. Every
337
+ * term settles on failure. jb.waitReady, the app's ready marker and
272
338
  * screenshot tools read it for a view.
273
339
  */
274
340
  get showLoading() {
341
+ const { connectedView, connectedTranscript, connectedFeature, connectionError, } = self;
275
342
  return (!self.error &&
276
343
  (isLaunching(self) ||
277
344
  (self.loadingStoredData && !self.dataInitialized) ||
278
- self.isLoading));
345
+ self.isLoading ||
346
+ self.loadingDomains ||
347
+ (!!connectedTranscript &&
348
+ !connectedFeature &&
349
+ connectionError === undefined) ||
350
+ (self.domainsOnGenome &&
351
+ self.connectedDomains.features.length > 0 &&
352
+ !!connectedView?.showLoading)));
353
+ },
354
+ }))
355
+ .views(self => ({
356
+ /**
357
+ * #getter
358
+ * overrides base
359
+ *
360
+ * react-msaview's load and data warnings, plus the plugin's link
361
+ * notices, which the header's warning chip lists and dismisses alike
362
+ */
363
+ get warnings() {
364
+ return [
365
+ ...self.loadWarnings,
366
+ ...[...self.dataWarnings, ...self.linkNotices].filter(w => !self.dismissedWarnings.includes(w)),
367
+ ];
368
+ },
369
+ }))
370
+ .views(self => ({
371
+ /**
372
+ * #getter
373
+ * what failed and left the view working: the messages the header's
374
+ * warning chip lists. A notice does not end `showLoading`.
375
+ */
376
+ get notices() {
377
+ return self.warnings;
378
+ },
379
+ }))
380
+ .views(self => ({
381
+ /**
382
+ * #getter
383
+ * what the view resolved to, as plain JSON. See docs/reading-the-view.md
384
+ */
385
+ get report() {
386
+ return viewReport(self);
279
387
  },
280
388
  }))
281
389
  .actions(self => ({
@@ -428,6 +536,34 @@ export default function stateModelFactory() {
428
536
  setDomainsRequested(arg) {
429
537
  self.domainsRequested = arg;
430
538
  },
539
+ /**
540
+ * #action
541
+ */
542
+ setLoadingDomains(arg) {
543
+ self.loadingDomains = arg;
544
+ },
545
+ /**
546
+ * #action
547
+ * `connectedTranscript` could not be resolved. A launch waiting on it
548
+ * cannot run, so for one that is the view's error; an alignment from
549
+ * anywhere else is still worth showing, so then it is a notice.
550
+ */
551
+ failConnection(e) {
552
+ self.connectionError = e;
553
+ if (launchAwaitsTranscript(self)) {
554
+ self.setError(e);
555
+ }
556
+ },
557
+ /**
558
+ * #action
559
+ */
560
+ clearConnectionFailure() {
561
+ if (self.connectionError !== undefined &&
562
+ self.error === self.connectionError) {
563
+ self.setError(undefined);
564
+ }
565
+ self.connectionError = undefined;
566
+ },
431
567
  /**
432
568
  * #action
433
569
  */
@@ -528,6 +664,24 @@ export default function stateModelFactory() {
528
664
  self.setDomainsRequested(false);
529
665
  self.setLastStoredData(undefined);
530
666
  self.setOwnsDataStoreRow(false);
667
+ self.clearConnectionFailure();
668
+ },
669
+ };
670
+ })
671
+ .actions(self => {
672
+ const superClearWarnings = self.clearWarnings.bind(self);
673
+ return {
674
+ /**
675
+ * #action
676
+ * overrides base, so dismissing the warning chip hides the link
677
+ * notices too until their cause changes
678
+ */
679
+ clearWarnings() {
680
+ superClearWarnings();
681
+ self.dismissedWarnings = [
682
+ ...self.dismissedWarnings,
683
+ ...self.linkNotices,
684
+ ];
531
685
  },
532
686
  };
533
687
  })