jbrowse-plugin-msaview 2.7.0 → 2.7.2

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.
@@ -0,0 +1 @@
1
+ export declare function useLocalStorage<T>(key: string, initialValue: T): readonly [T, (value: T) => void];
@@ -0,0 +1,29 @@
1
+ import { useState } from 'react';
2
+ // Vendored rather than imported from `@jbrowse/core/util`: that barrel is
3
+ // host-provided, and a barrel split dropped this export, making the BLAST panel
4
+ // throw "(0, PR.useLocalStorage) is not a function" on hosts built during that
5
+ // window. Same failure mode as `defaultCodonTable`; keeping our own copy takes
6
+ // this plugin out of the whack-a-mole.
7
+ function readLocalStorage(key, initialValue) {
8
+ try {
9
+ const item = globalThis.localStorage.getItem(key);
10
+ return item === null ? initialValue : JSON.parse(item);
11
+ }
12
+ catch (error) {
13
+ console.error(error);
14
+ return initialValue;
15
+ }
16
+ }
17
+ export function useLocalStorage(key, initialValue) {
18
+ const [storedValue, setStoredValue] = useState(() => readLocalStorage(key, initialValue));
19
+ const setValue = (value) => {
20
+ setStoredValue(value);
21
+ try {
22
+ globalThis.localStorage.setItem(key, JSON.stringify(value));
23
+ }
24
+ catch (error) {
25
+ console.error(error);
26
+ }
27
+ };
28
+ return [storedValue, setValue];
29
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "2.7.0";
1
+ export declare const version = "2.7.2";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.7.0';
1
+ export const version = '2.7.2';
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.7.0",
2
+ "version": "2.7.2",
3
3
  "license": "MIT",
4
4
  "name": "jbrowse-plugin-msaview",
5
5
  "repository": {
@@ -20,7 +20,6 @@
20
20
  "@gmod/bgzf-filehandle": "^6.2.0",
21
21
  "g2p_mapper": "^2.1.5",
22
22
  "idb": "^8.0.3",
23
- "react-msaview": "^5.6.1",
24
23
  "swr": "^2.4.2"
25
24
  },
26
25
  "devDependencies": {
@@ -52,6 +51,7 @@
52
51
  "puppeteer": "^25.3.0",
53
52
  "react": "^19.2.8",
54
53
  "react-dom": "^19.2.8",
54
+ "react-msaview": "^5.6.3",
55
55
  "rimraf": "^6.1.3",
56
56
  "rxjs": "^7.8.2",
57
57
  "serve": "^14.2.6",
@@ -74,7 +74,9 @@
74
74
  "test:setup:version": "node scripts/test-versions.mjs setup",
75
75
  "test:versions": "node scripts/test-versions.mjs run",
76
76
  "test:version": "node scripts/test-versions.mjs run",
77
- "preversion": "pnpm lint",
77
+ "host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-msaview.umd.production.min.js",
78
+ "check-ci": "node scripts/require-green-ci.mjs",
79
+ "preversion": "pnpm check-ci && pnpm lint && pnpm build && pnpm host-compat",
78
80
  "version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git add src/version.ts",
79
81
  "postversion": "git push --follow-tags"
80
82
  }
@@ -1,6 +1,5 @@
1
1
  import React, { useState } from 'react'
2
2
 
3
- import { useLocalStorage } from '@jbrowse/core/util'
4
3
  import SettingsIcon from '@mui/icons-material/Settings'
5
4
  import { IconButton } from '@mui/material'
6
5
  import { makeStyles } from 'tss-react/mui'
@@ -11,6 +10,7 @@ import NCBIBlastMethodSelector from './NCBIBlastMethodSelector'
11
10
  import NCBIBlastRIDPanel from './NCBIBlastRIDPanel'
12
11
  import NCBISettingsDialog from './NCBISettingsDialog'
13
12
  import { BASE_BLAST_URL } from './consts'
13
+ import { useLocalStorage } from '../../../utils/useLocalStorage'
14
14
 
15
15
  import type { AbstractTrackModel, Feature } from '@jbrowse/core/util'
16
16
 
@@ -1,35 +1,39 @@
1
- import {
2
- dedupe,
3
- defaultCodonTable,
4
- generateCodonTable,
5
- revcom,
6
- } from '@jbrowse/core/util'
1
+ import { dedupe, revcom } from '@jbrowse/core/util'
2
+ import { convertCodingSequenceToPeptides } from '@jbrowse/core/util/convertCodingSequenceToPeptides'
3
+
4
+ import { getGeneticCode, parseTranslTable } from './geneticCodes'
7
5
 
8
6
  import type { Feat } from './types'
9
7
  import type { Feature } from '@jbrowse/core/util'
10
8
 
11
- // pure constant: the standard codon table never varies, so build it once rather
12
- // than on every translation (which runs on every panel re-render)
13
- const codonTable = generateCodonTable(defaultCodonTable)
14
-
15
- export function stitch(subfeats: Feat[], sequence: string) {
16
- return subfeats.map(sub => sequence.slice(sub.start, sub.end)).join('')
17
- }
9
+ // `@jbrowse/core/util/convertCodingSequenceToPeptides` is a deep path, so unlike
10
+ // the `@jbrowse/core/util` barrel it is absent from ReExports and gets bundled
11
+ // rather than resolved out of the host's JBrowseExports. That is what makes
12
+ // reusing core's translation safe across every host a config names: this module
13
+ // previously built its codon table at module scope from the barrel's
14
+ // `defaultCodonTable`, and a core build that dropped that export turned it into
15
+ // `Object.keys(undefined)` while the UMD was still evaluating -- the plugin
16
+ // global was never assigned and PluginLoader error-paged the whole app.
18
17
 
19
18
  export function calculateProteinSequence({
20
19
  cds,
21
20
  sequence,
21
+ geneticCodeId,
22
22
  }: {
23
23
  cds: Feat[]
24
24
  sequence: string
25
+ geneticCodeId?: number
25
26
  }) {
26
- const str = stitch(cds, sequence)
27
- let protein = ''
28
- for (let i = 0; i < str.length; i += 3) {
29
- // use & symbol for undefined codon, or partial slice
30
- protein += codonTable[str.slice(i, i + 3)] ?? '&'
31
- }
32
- return protein
27
+ // `starts` is deliberately not passed: @jbrowse/core 4.3.0's signature has no
28
+ // such parameter, so alternative initiators (GTG under table 11, ATA under
29
+ // table 2) render as their internal residue rather than M. Core main added it;
30
+ // pass it here when msaview's @jbrowse/core floor reaches that release.
31
+ const { codonTable } = getGeneticCode(geneticCodeId)
32
+ return convertCodingSequenceToPeptides({
33
+ cds,
34
+ sequence,
35
+ codonTable,
36
+ })
33
37
  }
34
38
 
35
39
  export function revlist(list: Feat[], seqlen: number) {
@@ -62,8 +66,19 @@ export function getProteinSequenceFromFeature({
62
66
  feat => `${feat.start}-${feat.end}`,
63
67
  )
64
68
 
69
+ // a mitochondrial gene declares e.g. transl_table=2, so it translates with
70
+ // NCBI table 2 rather than the standard code. GFF3 usually carries the
71
+ // attribute on the CDS rather than the transcript, so check both.
72
+ const cdsSubfeature = feature
73
+ .get('subfeatures')
74
+ ?.find((f: Feature) => f.get('type')?.toLowerCase() === 'cds')
75
+ const geneticCodeId =
76
+ parseTranslTable(feature.get('transl_table')) ??
77
+ parseTranslTable(cdsSubfeature?.get('transl_table'))
78
+
65
79
  return calculateProteinSequence({
66
80
  cds: strand === -1 ? revlist(cds, seq.length) : cds,
67
81
  sequence: strand === -1 ? revcom(seq) : seq,
82
+ geneticCodeId,
68
83
  })
69
84
  }
@@ -0,0 +1,298 @@
1
+ // NCBI translation tables (genetic codes), copied verbatim from
2
+ // jbrowse-components packages/core/src/util/geneticCodes.ts, which parses them
3
+ // from NCBI's authoritative gc.prt. `ncbieaa` gives the amino acid for each of
4
+ // the 64 codons in a fixed base order; `sncbieaa` marks valid start codons.
5
+ //
6
+ // Vendored rather than imported: core exports this only on main, and the msaview
7
+ // bundle has to translate the same way on every host a config names, back to
8
+ // v4.0.0. Replace the whole file with a re-export of
9
+ // `@jbrowse/core/util/geneticCodes` once a release ships it -- that path is not
10
+ // in ReExports, so it bundles rather than binding to the host.
11
+ export interface NcbiGeneticCode {
12
+ id: number
13
+ name: string
14
+ ncbieaa: string
15
+ sncbieaa: string
16
+ }
17
+
18
+ export const ncbiGeneticCodes: NcbiGeneticCode[] = [
19
+ {
20
+ id: 1,
21
+ name: 'Standard',
22
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
23
+ sncbieaa:
24
+ '---M------**--*----M---------------M----------------------------',
25
+ },
26
+ {
27
+ id: 2,
28
+ name: 'Vertebrate Mitochondrial',
29
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSS**VVVVAAAADDEEGGGG',
30
+ sncbieaa:
31
+ '----------**--------------------MMMM----------**---M------------',
32
+ },
33
+ {
34
+ id: 3,
35
+ name: 'Yeast Mitochondrial',
36
+ ncbieaa: 'FFLLSSSSYY**CCWWTTTTPPPPHHQQRRRRIIMMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
37
+ sncbieaa:
38
+ '----------**----------------------MM---------------M------------',
39
+ },
40
+ {
41
+ id: 4,
42
+ name: 'Mold Mitochondrial; Protozoan Mitochondrial; Coelenterate Mitochondrial; Mycoplasma; Spiroplasma',
43
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
44
+ sncbieaa:
45
+ '--MM------**-------M------------MMMM---------------M------------',
46
+ },
47
+ {
48
+ id: 5,
49
+ name: 'Invertebrate Mitochondrial',
50
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSSSVVVVAAAADDEEGGGG',
51
+ sncbieaa:
52
+ '---M------**--------------------MMMM---------------M------------',
53
+ },
54
+ {
55
+ id: 6,
56
+ name: 'Ciliate Nuclear; Dasycladacean Nuclear; Hexamita Nuclear',
57
+ ncbieaa: 'FFLLSSSSYYQQCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
58
+ sncbieaa:
59
+ '--------------*--------------------M----------------------------',
60
+ },
61
+ {
62
+ id: 9,
63
+ name: 'Echinoderm Mitochondrial; Flatworm Mitochondrial',
64
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
65
+ sncbieaa:
66
+ '----------**-----------------------M---------------M------------',
67
+ },
68
+ {
69
+ id: 10,
70
+ name: 'Euplotid Nuclear',
71
+ ncbieaa: 'FFLLSSSSYY**CCCWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
72
+ sncbieaa:
73
+ '----------**-----------------------M----------------------------',
74
+ },
75
+ {
76
+ id: 11,
77
+ name: 'Bacterial, Archaeal and Plant Plastid',
78
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
79
+ sncbieaa:
80
+ '---M------**--*----M------------MMMM---------------M------------',
81
+ },
82
+ {
83
+ id: 12,
84
+ name: 'Alternative Yeast Nuclear',
85
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLSPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
86
+ sncbieaa:
87
+ '----------**--*----M---------------M----------------------------',
88
+ },
89
+ {
90
+ id: 13,
91
+ name: 'Ascidian Mitochondrial',
92
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNKKSSGGVVVVAAAADDEEGGGG',
93
+ sncbieaa:
94
+ '---M------**----------------------MM---------------M------------',
95
+ },
96
+ {
97
+ id: 14,
98
+ name: 'Alternative Flatworm Mitochondrial',
99
+ ncbieaa: 'FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
100
+ sncbieaa:
101
+ '-----------*-----------------------M----------------------------',
102
+ },
103
+ {
104
+ id: 15,
105
+ name: 'Blepharisma Macronuclear',
106
+ ncbieaa: 'FFLLSSSSYY*QCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
107
+ sncbieaa:
108
+ '----------*---*--------------------M----------------------------',
109
+ },
110
+ {
111
+ id: 16,
112
+ name: 'Chlorophycean Mitochondrial',
113
+ ncbieaa: 'FFLLSSSSYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
114
+ sncbieaa:
115
+ '----------*---*--------------------M----------------------------',
116
+ },
117
+ {
118
+ id: 21,
119
+ name: 'Trematode Mitochondrial',
120
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIMMTTTTNNNKSSSSVVVVAAAADDEEGGGG',
121
+ sncbieaa:
122
+ '----------**-----------------------M---------------M------------',
123
+ },
124
+ {
125
+ id: 22,
126
+ name: 'Scenedesmus obliquus Mitochondrial',
127
+ ncbieaa: 'FFLLSS*SYY*LCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
128
+ sncbieaa:
129
+ '------*---*---*--------------------M----------------------------',
130
+ },
131
+ {
132
+ id: 23,
133
+ name: 'Thraustochytrium Mitochondrial',
134
+ ncbieaa: 'FF*LSSSSYY**CC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
135
+ sncbieaa:
136
+ '--*-------**--*-----------------M--M---------------M------------',
137
+ },
138
+ {
139
+ id: 24,
140
+ name: 'Rhabdopleuridae Mitochondrial',
141
+ ncbieaa: 'FFLLSSSSYY**CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG',
142
+ sncbieaa:
143
+ '---M------**-------M---------------M---------------M------------',
144
+ },
145
+ {
146
+ id: 25,
147
+ name: 'Candidate Division SR1 and Gracilibacteria',
148
+ ncbieaa: 'FFLLSSSSYY**CCGWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
149
+ sncbieaa:
150
+ '---M------**-----------------------M---------------M------------',
151
+ },
152
+ {
153
+ id: 26,
154
+ name: 'Pachysolen tannophilus Nuclear',
155
+ ncbieaa: 'FFLLSSSSYY**CC*WLLLAPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
156
+ sncbieaa:
157
+ '----------**--*----M---------------M----------------------------',
158
+ },
159
+ {
160
+ id: 27,
161
+ name: 'Karyorelict Nuclear',
162
+ ncbieaa: 'FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
163
+ sncbieaa:
164
+ '--------------*--------------------M----------------------------',
165
+ },
166
+ {
167
+ id: 28,
168
+ name: 'Condylostoma Nuclear',
169
+ ncbieaa: 'FFLLSSSSYYQQCCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
170
+ sncbieaa:
171
+ '----------**--*--------------------M----------------------------',
172
+ },
173
+ {
174
+ id: 29,
175
+ name: 'Mesodinium Nuclear',
176
+ ncbieaa: 'FFLLSSSSYYYYCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
177
+ sncbieaa:
178
+ '--------------*--------------------M----------------------------',
179
+ },
180
+ {
181
+ id: 30,
182
+ name: 'Peritrich Nuclear',
183
+ ncbieaa: 'FFLLSSSSYYEECC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
184
+ sncbieaa:
185
+ '--------------*--------------------M----------------------------',
186
+ },
187
+ {
188
+ id: 31,
189
+ name: 'Blastocrithidia Nuclear',
190
+ ncbieaa: 'FFLLSSSSYYEECCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
191
+ sncbieaa:
192
+ '----------**-----------------------M----------------------------',
193
+ },
194
+ {
195
+ id: 32,
196
+ name: 'Balanophoraceae Plastid',
197
+ ncbieaa: 'FFLLSSSSYY*WCC*WLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSRRVVVVAAAADDEEGGGG',
198
+ sncbieaa:
199
+ '---M------*---*----M------------MMMM---------------M------------',
200
+ },
201
+ {
202
+ id: 33,
203
+ name: 'Cephalodiscidae Mitochondrial',
204
+ ncbieaa: 'FFLLSSSSYYY*CCWWLLLLPPPPHHQQRRRRIIIMTTTTNNKKSSSKVVVVAAAADDEEGGGG',
205
+ sncbieaa:
206
+ '---M-------*-------M---------------M---------------M------------',
207
+ },
208
+ ]
209
+
210
+ // The codon order shared by every NCBI table -- the Base1/Base2/Base3 comment
211
+ // rows in gc.prt. codon i = BASE1[i] + BASE2[i] + BASE3[i].
212
+ const BASE1 = 'TTTTTTTTTTTTTTTTCCCCCCCCCCCCCCCCAAAAAAAAAAAAAAAAGGGGGGGGGGGGGGGG'
213
+ const BASE2 = 'TTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGGTTTTCCCCAAAAGGGG'
214
+ const BASE3 = 'TCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAGTCAG'
215
+
216
+ const CODONS = Array.from(
217
+ { length: 64 },
218
+ (_, i) => BASE1[i]! + BASE2[i]! + BASE3[i]!,
219
+ )
220
+
221
+ const ncbiCodeById = new Map(ncbiGeneticCodes.map(t => [t.id, t]))
222
+
223
+ export interface GeneticCode {
224
+ id: number
225
+ name: string
226
+ // case-insensitive codon -> amino acid letter, '*' for a stop codon
227
+ codonTable: Record<string, string>
228
+ // valid start codons (uppercase)
229
+ starts: string[]
230
+ }
231
+
232
+ // Expand an uppercase codon map so every case combination of a triplet resolves,
233
+ // which is what callers reading raw sequence need.
234
+ function caseExpand(table: Record<string, string>) {
235
+ const out: Record<string, string> = {}
236
+ for (const [codon, aa] of Object.entries(table)) {
237
+ const cases = (i: number) => {
238
+ const n = codon.charAt(i)
239
+ return [n.toUpperCase(), n.toLowerCase()]
240
+ }
241
+ for (const n0 of cases(0)) {
242
+ for (const n1 of cases(1)) {
243
+ for (const n2 of cases(2)) {
244
+ out[n0 + n1 + n2] = aa
245
+ }
246
+ }
247
+ }
248
+ }
249
+ return out
250
+ }
251
+
252
+ function buildGeneticCode(id: number): GeneticCode {
253
+ const def = ncbiCodeById.get(id)
254
+ if (!def && id !== 1) {
255
+ console.warn(
256
+ `Unknown genetic code (transl_table=${id}); using standard code`,
257
+ )
258
+ }
259
+ const {
260
+ id: resolvedId,
261
+ name,
262
+ ncbieaa,
263
+ sncbieaa,
264
+ } = def ?? ncbiCodeById.get(1)!
265
+ const table: Record<string, string> = {}
266
+ const starts: string[] = []
267
+ for (const [i, codon] of CODONS.entries()) {
268
+ table[codon] = ncbieaa[i]!
269
+ if (sncbieaa[i] === 'M') {
270
+ starts.push(codon)
271
+ }
272
+ }
273
+ return { id: resolvedId, name, codonTable: caseExpand(table), starts }
274
+ }
275
+
276
+ const geneticCodeCache = new Map<number, GeneticCode>()
277
+
278
+ // Resolves the codon map + start set for an NCBI translation-table id, falling
279
+ // back to the standard code (1) for an unrecognized id. Memoized: there are only
280
+ // ~27 tables and each result is immutable.
281
+ export function getGeneticCode(id = 1): GeneticCode {
282
+ let code = geneticCodeCache.get(id)
283
+ if (!code) {
284
+ code = buildGeneticCode(id)
285
+ geneticCodeCache.set(id, code)
286
+ }
287
+ return code
288
+ }
289
+
290
+ // Parses a GFF/GenBank `transl_table` attribute value into an NCBI table id. The
291
+ // GFF adapter yields a string (or an array if the attribute repeated), so this
292
+ // normalizes both; returns undefined for a missing or non-positive-integer value
293
+ // so callers fall back to their default code.
294
+ export function parseTranslTable(value: unknown): number | undefined {
295
+ const raw = Array.isArray(value) ? value[0] : value
296
+ const n = Number(raw)
297
+ return Number.isInteger(n) && n > 0 ? n : undefined
298
+ }
@@ -2,6 +2,9 @@ export interface Feat {
2
2
  start: number
3
3
  end: number
4
4
  type?: string
5
+ // GFF phase of the first coding base; convertCodingSequenceToPeptides reads it
6
+ // off cds[0] to start translation in the right frame
7
+ phase?: number
5
8
  }
6
9
 
7
10
  export interface SeqState {
@@ -15,8 +15,9 @@ function isDisplay(elt: { name: string }): elt is DisplayType {
15
15
  }
16
16
 
17
17
  // The canvas LinearBasicDisplay (JBrowse >=4.3) exposes the right-clicked
18
- // feature via contextMenuInfo + async fetchFullFeature rather than a synchronous
19
- // feature object.
18
+ // feature via contextMenuInfo + async fetchFullFeature. Hosts before that -- and
19
+ // the v3.7.0 in the wild that shipped configs still name -- expose it
20
+ // synchronously as contextMenuFeature, and only have that one.
20
21
  interface ContextMenuInfo {
21
22
  item: { featureId: string; type?: string }
22
23
  displayedRegionIndex: number
@@ -25,13 +26,16 @@ interface ContextMenuInfo {
25
26
  interface DisplayModel {
26
27
  contextMenuItems: () => MenuItem[]
27
28
  contextMenuInfo?: ContextMenuInfo
28
- isGeneLike: boolean
29
- fetchFullFeature: (
29
+ isGeneLike?: boolean
30
+ fetchFullFeature?: (
30
31
  featureId: string,
31
32
  displayedRegionIndex: number,
32
33
  ) => Promise<Feature | undefined>
34
+ contextMenuFeature?: Feature
33
35
  }
34
36
 
37
+ const GENE_LIKE_TYPES = new Set(['gene', 'mRNA', 'transcript'])
38
+
35
39
  function extendStateModel(stateModel: IAnyModelType) {
36
40
  return stateModel.views((self: DisplayModel) => {
37
41
  const superContextMenuItems = self.contextMenuItems
@@ -39,40 +43,45 @@ function extendStateModel(stateModel: IAnyModelType) {
39
43
  contextMenuItems() {
40
44
  const track = getContainingTrack(self)
41
45
  const session = getSession(track)
46
+ const launch = (feature: Feature) => {
47
+ session.queueDialog(handleClose => [
48
+ LaunchMsaViewDialog,
49
+ { model: track, handleClose, feature },
50
+ ])
51
+ }
52
+
42
53
  const info = self.contextMenuInfo
43
- const showMsaMenuItem = info && self.isGeneLike
54
+ const fetchFullFeature = self.fetchFullFeature
55
+ const legacyFeature = self.contextMenuFeature
56
+ const onClick =
57
+ info && fetchFullFeature && self.isGeneLike
58
+ ? () => {
59
+ fetchFullFeature(info.item.featureId, info.displayedRegionIndex)
60
+ .then(feature => {
61
+ if (feature) {
62
+ launch(feature)
63
+ } else {
64
+ session.notify(
65
+ 'Could not load feature for MSA view',
66
+ 'warning',
67
+ )
68
+ }
69
+ })
70
+ .catch((e: unknown) => {
71
+ session.notifyError(`${e}`, e)
72
+ })
73
+ }
74
+ : legacyFeature &&
75
+ GENE_LIKE_TYPES.has(String(legacyFeature.get('type')))
76
+ ? () => {
77
+ launch(legacyFeature)
78
+ }
79
+ : undefined
80
+
44
81
  return [
45
82
  ...superContextMenuItems(),
46
- ...(showMsaMenuItem
47
- ? [
48
- {
49
- label: 'Launch MSA view',
50
- icon: AddIcon,
51
- onClick: () => {
52
- self
53
- .fetchFullFeature(
54
- info.item.featureId,
55
- info.displayedRegionIndex,
56
- )
57
- .then(feature => {
58
- if (feature) {
59
- session.queueDialog(handleClose => [
60
- LaunchMsaViewDialog,
61
- { model: track, handleClose, feature },
62
- ])
63
- } else {
64
- session.notify(
65
- 'Could not load feature for MSA view',
66
- 'warning',
67
- )
68
- }
69
- })
70
- .catch((e: unknown) => {
71
- session.notifyError(`${e}`, e)
72
- })
73
- },
74
- },
75
- ]
83
+ ...(onClick
84
+ ? [{ label: 'Launch MSA view', icon: AddIcon, onClick }]
76
85
  : []),
77
86
  ]
78
87
  },
@@ -77,7 +77,12 @@ export default function stateModelFactory() {
77
77
  /**
78
78
  * #property
79
79
  */
80
- querySeqName: types.stripDefault(types.string, 'QUERY'),
80
+ // Plain defaults, not types.stripDefault: that postdates the
81
+ // mobx-state-tree every released core exposes (present only on main), and
82
+ // the missing function throws while this model is being built, which
83
+ // error-pages the whole app rather than just this view. Restore
84
+ // stripDefault once a release ships it.
85
+ querySeqName: 'QUERY',
81
86
 
82
87
  /**
83
88
  * #property
@@ -87,7 +92,8 @@ export default function stateModelFactory() {
87
92
  /**
88
93
  * #property
89
94
  */
90
- zoomToBaseLevel: types.stripDefault(types.boolean, false),
95
+ // see querySeqName above re: types.stripDefault
96
+ zoomToBaseLevel: false,
91
97
 
92
98
  /**
93
99
  * #property
@@ -0,0 +1,31 @@
1
+ import { useState } from 'react'
2
+
3
+ // Vendored rather than imported from `@jbrowse/core/util`: that barrel is
4
+ // host-provided, and a barrel split dropped this export, making the BLAST panel
5
+ // throw "(0, PR.useLocalStorage) is not a function" on hosts built during that
6
+ // window. Same failure mode as `defaultCodonTable`; keeping our own copy takes
7
+ // this plugin out of the whack-a-mole.
8
+ function readLocalStorage<T>(key: string, initialValue: T): T {
9
+ try {
10
+ const item = globalThis.localStorage.getItem(key)
11
+ return item === null ? initialValue : (JSON.parse(item) as T)
12
+ } catch (error) {
13
+ console.error(error)
14
+ return initialValue
15
+ }
16
+ }
17
+
18
+ export function useLocalStorage<T>(key: string, initialValue: T) {
19
+ const [storedValue, setStoredValue] = useState<T>(() =>
20
+ readLocalStorage(key, initialValue),
21
+ )
22
+ const setValue = (value: T) => {
23
+ setStoredValue(value)
24
+ try {
25
+ globalThis.localStorage.setItem(key, JSON.stringify(value))
26
+ } catch (error) {
27
+ console.error(error)
28
+ }
29
+ }
30
+ return [storedValue, setValue] as const
31
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '2.7.0'
1
+ export const version = '2.7.2'