jbrowse-plugin-msaview 2.10.0 → 2.10.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.
- package/dist/LaunchMsaView/components/BlastQuery/useCachedBlastResults.d.ts +1 -1
- package/dist/LaunchMsaView/components/BlastQuery/useCachedBlastResults.js +14 -17
- package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +1 -1
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.d.ts +2 -1
- package/dist/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.js +48 -50
- package/dist/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.js +4 -5
- package/dist/LaunchMsaView/components/useFeatureSequence.d.ts +6 -4
- package/dist/LaunchMsaView/components/useFeatureSequence.js +20 -11
- package/dist/LaunchMsaView/components/useTranscriptSelection.d.ts +1 -1
- package/dist/LaunchMsaView/extendStateModel.test.d.ts +1 -0
- package/dist/LaunchMsaView/extendStateModel.test.js +69 -0
- package/dist/LaunchMsaView/index.d.ts +2 -0
- package/dist/LaunchMsaView/index.js +44 -54
- package/dist/LaunchMsaView/launchTarget.d.ts +25 -0
- package/dist/LaunchMsaView/launchTarget.js +42 -0
- package/dist/LaunchMsaView/launchTarget.test.d.ts +1 -0
- package/dist/LaunchMsaView/launchTarget.test.js +54 -0
- package/dist/MsaViewPanel/doLaunchOrthologs.js +4 -2
- package/dist/MsaViewPanel/model.d.ts +4 -2
- package/dist/jbrowse-plugin-msaview.umd.production.min.js +29 -40
- package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
- package/dist/utils/ebiJobDispatcher.test.js +10 -1
- package/dist/utils/ncbiOrthologs.d.ts +1 -1
- package/dist/utils/ncbiOrthologs.js +1 -1
- package/dist/utils/ncbiTaxonomy.d.ts +19 -0
- package/dist/utils/ncbiTaxonomy.js +42 -9
- package/dist/utils/ncbiTaxonomy.test.d.ts +1 -0
- package/dist/utils/ncbiTaxonomy.test.js +69 -0
- package/dist/utils/useFetch.d.ts +22 -0
- package/dist/utils/useFetch.js +79 -0
- package/dist/utils/useFetch.test.d.ts +1 -0
- package/dist/utils/useFetch.test.js +23 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/src/LaunchMsaView/components/BlastQuery/useCachedBlastResults.ts +16 -29
- package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +1 -0
- package/src/LaunchMsaView/components/OrthologQuery/QuerySpeciesSelect.tsx +63 -49
- package/src/LaunchMsaView/components/PreLoadedMSA/PreLoadedMSADataPanel.tsx +3 -6
- package/src/LaunchMsaView/components/useFeatureSequence.ts +35 -15
- package/src/LaunchMsaView/extendStateModel.test.ts +76 -0
- package/src/LaunchMsaView/index.ts +47 -80
- package/src/LaunchMsaView/launchTarget.test.ts +77 -0
- package/src/LaunchMsaView/launchTarget.ts +65 -0
- package/src/MsaViewPanel/doLaunchOrthologs.ts +4 -2
- package/src/MsaViewPanel/model.ts +4 -2
- package/src/utils/ebiJobDispatcher.test.ts +10 -1
- package/src/utils/ncbiOrthologs.ts +1 -1
- package/src/utils/ncbiTaxonomy.test.ts +84 -0
- package/src/utils/ncbiTaxonomy.ts +52 -9
- package/src/utils/useFetch.test.ts +30 -0
- package/src/utils/useFetch.ts +116 -0
- package/src/version.ts +1 -1
- package/dist/LaunchMsaView/components/useSWRFeatureSequence.d.ts +0 -12
- package/dist/LaunchMsaView/components/useSWRFeatureSequence.js +0 -25
- package/dist/utils/swrConfig.d.ts +0 -8
- package/dist/utils/swrConfig.js +0 -8
- package/src/LaunchMsaView/components/useSWRFeatureSequence.ts +0 -54
- package/src/utils/swrConfig.ts +0 -8
|
@@ -32,7 +32,16 @@ describe('waitForEbiJob', () => {
|
|
|
32
32
|
});
|
|
33
33
|
test('failures that do not persist never accumulate to the limit', async () => {
|
|
34
34
|
const blip = new TypeError('Failed to fetch');
|
|
35
|
-
statuses([
|
|
35
|
+
statuses([
|
|
36
|
+
blip,
|
|
37
|
+
'RUNNING',
|
|
38
|
+
blip,
|
|
39
|
+
'RUNNING',
|
|
40
|
+
blip,
|
|
41
|
+
'RUNNING',
|
|
42
|
+
blip,
|
|
43
|
+
'FINISHED',
|
|
44
|
+
]);
|
|
36
45
|
await expect(wait()).resolves.toBeUndefined();
|
|
37
46
|
});
|
|
38
47
|
test('an endpoint that has genuinely gone away is not polled forever', async () => {
|
|
@@ -26,7 +26,7 @@ export declare function resolveGeneId(candidates: string[], taxId: number): Prom
|
|
|
26
26
|
*
|
|
27
27
|
* `taxa` narrows the set when a caller wants specific species; omitted, every
|
|
28
28
|
* species NCBI has an ortholog for is a candidate. `exclude` drops the query
|
|
29
|
-
* taxon, which
|
|
29
|
+
* taxon, which already has its own `<species>_query` row.
|
|
30
30
|
*/
|
|
31
31
|
export declare function fetchOrthologGenes(geneId: string, { taxa, exclude, limit, }?: {
|
|
32
32
|
taxa?: Set<number>;
|
|
@@ -85,7 +85,7 @@ export async function resolveGeneId(candidates, taxId) {
|
|
|
85
85
|
*
|
|
86
86
|
* `taxa` narrows the set when a caller wants specific species; omitted, every
|
|
87
87
|
* species NCBI has an ortholog for is a candidate. `exclude` drops the query
|
|
88
|
-
* taxon, which
|
|
88
|
+
* taxon, which already has its own `<species>_query` row.
|
|
89
89
|
*/
|
|
90
90
|
export async function fetchOrthologGenes(geneId, { taxa, exclude, limit = defaultMaxSpecies, } = {}) {
|
|
91
91
|
const json = await jsonfetch(ncbiUrl(`${DATASETS}/gene/id/${geneId}/orthologs?returned_content=COMPLETE`));
|
|
@@ -9,3 +9,22 @@
|
|
|
9
9
|
* species silently resolves the wrong organism's gene for anyone outside it.
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveTaxId(query: string): Promise<number | undefined>;
|
|
12
|
+
/**
|
|
13
|
+
* A JBrowse assembly name -> the species it is an assembly of.
|
|
14
|
+
*
|
|
15
|
+
* The dialog's query species used to open on `human` whoever was browsing, and
|
|
16
|
+
* that is the same silent wrong answer the 23-species list gave: `resolveGeneId`
|
|
17
|
+
* searches `SYMBOL[Gene Name] AND <taxid>[taxid]`, so a user right-clicking a
|
|
18
|
+
* mouse gene got the HUMAN gene of that symbol, its orthologs, and mouse twice
|
|
19
|
+
* — once as the query row and once as an ortholog, because the excluded taxon
|
|
20
|
+
* was the wrong one. A symbol with no human counterpart failed outright.
|
|
21
|
+
*
|
|
22
|
+
* db=assembly indexes the names JBrowse configs actually carry: hg38, GRCh38,
|
|
23
|
+
* mm39, danRer11, dm6 and sacCer3 all resolve, and so do RefSeq/GenBank
|
|
24
|
+
* accessions. An assembly NCBI has never heard of resolves to nothing and the
|
|
25
|
+
* caller keeps its own default, so this can only improve on guessing.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveAssemblySpecies(assemblyName: string): Promise<{
|
|
28
|
+
taxId: number;
|
|
29
|
+
speciesName: string;
|
|
30
|
+
} | undefined>;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { NCBI_EMAIL, NCBI_TOOL } from './eutils';
|
|
2
2
|
import { jsonfetch } from './fetch';
|
|
3
3
|
const EUTILS = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
4
|
+
function eutilsUrl(endpoint, params) {
|
|
5
|
+
const search = new URLSearchParams({
|
|
6
|
+
...params,
|
|
7
|
+
retmode: 'json',
|
|
8
|
+
tool: NCBI_TOOL,
|
|
9
|
+
email: NCBI_EMAIL,
|
|
10
|
+
});
|
|
11
|
+
return `${EUTILS}/${endpoint}?${search.toString()}`;
|
|
12
|
+
}
|
|
4
13
|
/**
|
|
5
14
|
* Free text -> NCBI taxon id. A bare number is taken as the id itself; anything
|
|
6
15
|
* else is searched against db=taxonomy, which resolves a scientific name
|
|
@@ -19,15 +28,39 @@ export async function resolveTaxId(query) {
|
|
|
19
28
|
if (/^\d+$/.test(term)) {
|
|
20
29
|
return Number(term);
|
|
21
30
|
}
|
|
22
|
-
const
|
|
23
|
-
db: 'taxonomy',
|
|
24
|
-
term,
|
|
25
|
-
retmode: 'json',
|
|
26
|
-
retmax: '1',
|
|
27
|
-
tool: NCBI_TOOL,
|
|
28
|
-
email: NCBI_EMAIL,
|
|
29
|
-
});
|
|
30
|
-
const json = await jsonfetch(`${EUTILS}/esearch.fcgi?${search.toString()}`);
|
|
31
|
+
const json = await jsonfetch(eutilsUrl('esearch.fcgi', { db: 'taxonomy', term, retmax: '1' }));
|
|
31
32
|
const id = json.esearchresult?.idlist?.[0];
|
|
32
33
|
return id ? Number(id) : undefined;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A JBrowse assembly name -> the species it is an assembly of.
|
|
37
|
+
*
|
|
38
|
+
* The dialog's query species used to open on `human` whoever was browsing, and
|
|
39
|
+
* that is the same silent wrong answer the 23-species list gave: `resolveGeneId`
|
|
40
|
+
* searches `SYMBOL[Gene Name] AND <taxid>[taxid]`, so a user right-clicking a
|
|
41
|
+
* mouse gene got the HUMAN gene of that symbol, its orthologs, and mouse twice
|
|
42
|
+
* — once as the query row and once as an ortholog, because the excluded taxon
|
|
43
|
+
* was the wrong one. A symbol with no human counterpart failed outright.
|
|
44
|
+
*
|
|
45
|
+
* db=assembly indexes the names JBrowse configs actually carry: hg38, GRCh38,
|
|
46
|
+
* mm39, danRer11, dm6 and sacCer3 all resolve, and so do RefSeq/GenBank
|
|
47
|
+
* accessions. An assembly NCBI has never heard of resolves to nothing and the
|
|
48
|
+
* caller keeps its own default, so this can only improve on guessing.
|
|
49
|
+
*/
|
|
50
|
+
export async function resolveAssemblySpecies(assemblyName) {
|
|
51
|
+
const term = assemblyName.trim();
|
|
52
|
+
if (!term) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const search = await jsonfetch(eutilsUrl('esearch.fcgi', { db: 'assembly', term, retmax: '1' }));
|
|
56
|
+
const uid = search.esearchresult?.idlist?.[0];
|
|
57
|
+
if (!uid) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const summary = await jsonfetch(eutilsUrl('esummary.fcgi', { db: 'assembly', id: uid }));
|
|
61
|
+
const record = summary.result?.[uid];
|
|
62
|
+
const taxId = Number(record?.speciestaxid);
|
|
63
|
+
return record?.speciesname && taxId
|
|
64
|
+
? { taxId, speciesName: record.speciesname }
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { resolveAssemblySpecies, resolveTaxId } from './ncbiTaxonomy';
|
|
3
|
+
const stubFetch = (handler) => {
|
|
4
|
+
const seen = [];
|
|
5
|
+
vi.stubGlobal('fetch', (url) => {
|
|
6
|
+
seen.push(url);
|
|
7
|
+
return Promise.resolve({
|
|
8
|
+
ok: true,
|
|
9
|
+
status: 200,
|
|
10
|
+
json: () => Promise.resolve(handler(url)),
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
return seen;
|
|
14
|
+
};
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.unstubAllGlobals();
|
|
17
|
+
});
|
|
18
|
+
describe('resolveTaxId', () => {
|
|
19
|
+
test('takes a bare number as the id and spends no request on it', async () => {
|
|
20
|
+
const seen = stubFetch(() => ({}));
|
|
21
|
+
expect(await resolveTaxId(' 9606 ')).toBe(9606);
|
|
22
|
+
expect(seen).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
test('searches db=taxonomy for anything else', async () => {
|
|
25
|
+
const seen = stubFetch(() => ({ esearchresult: { idlist: ['7955'] } }));
|
|
26
|
+
expect(await resolveTaxId('zebrafish')).toBe(7955);
|
|
27
|
+
expect(seen[0]).toContain('db=taxonomy');
|
|
28
|
+
});
|
|
29
|
+
test('an unmatched name resolves to nothing', async () => {
|
|
30
|
+
stubFetch(() => ({ esearchresult: { idlist: [] } }));
|
|
31
|
+
expect(await resolveTaxId('not an organism')).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
// The dialog opened on `human` whoever was browsing, which on a mouse assembly
|
|
35
|
+
// resolves the gene symbol to the HUMAN gene and excludes the wrong taxon, so
|
|
36
|
+
// mouse lands twice. db=assembly is where the assembly names JBrowse configs
|
|
37
|
+
// carry are indexed.
|
|
38
|
+
describe('resolveAssemblySpecies', () => {
|
|
39
|
+
const summaryFor = (speciestaxid, speciesname) => ({
|
|
40
|
+
result: { uids: ['1'], '1': { speciestaxid, speciesname } },
|
|
41
|
+
});
|
|
42
|
+
test('resolves an assembly name to the species it is an assembly of', async () => {
|
|
43
|
+
const seen = stubFetch(url => url.includes('esearch')
|
|
44
|
+
? { esearchresult: { idlist: ['1'] } }
|
|
45
|
+
: summaryFor('10090', 'Mus musculus'));
|
|
46
|
+
expect(await resolveAssemblySpecies('mm39')).toEqual({
|
|
47
|
+
taxId: 10090,
|
|
48
|
+
speciesName: 'Mus musculus',
|
|
49
|
+
});
|
|
50
|
+
expect(seen[0]).toContain('db=assembly');
|
|
51
|
+
expect(seen[1]).toContain('esummary');
|
|
52
|
+
});
|
|
53
|
+
test('an assembly NCBI does not index resolves to nothing, without a summary call', async () => {
|
|
54
|
+
const seen = stubFetch(() => ({ esearchresult: { idlist: [] } }));
|
|
55
|
+
expect(await resolveAssemblySpecies('my_custom_asm')).toBeUndefined();
|
|
56
|
+
expect(seen).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
test('a summary carrying no species resolves to nothing rather than taxon NaN', async () => {
|
|
59
|
+
stubFetch(url => url.includes('esearch')
|
|
60
|
+
? { esearchresult: { idlist: ['1'] } }
|
|
61
|
+
: { result: { uids: ['1'], '1': {} } });
|
|
62
|
+
expect(await resolveAssemblySpecies('hg38')).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
test('an empty name asks nothing', async () => {
|
|
65
|
+
const seen = stubFetch(() => ({}));
|
|
66
|
+
expect(await resolveAssemblySpecies(' ')).toBeUndefined();
|
|
67
|
+
expect(seen).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type FetchKey = string | readonly unknown[] | null | undefined | false;
|
|
2
|
+
export declare function serializeKey(key: FetchKey): string | null;
|
|
3
|
+
/**
|
|
4
|
+
* Fetch once per key, keeping loading and error state with the data instead of
|
|
5
|
+
* spread across three `useState`s and a cancellation flag.
|
|
6
|
+
*
|
|
7
|
+
* Everything the caller reads is derived from the one settled result, which is
|
|
8
|
+
* what keeps the states consistent: while a key change is in flight there is no
|
|
9
|
+
* frame showing the previous key's data as though it were the new key's. A
|
|
10
|
+
* `mutate()` refetch under the same key does leave its data up, because it is
|
|
11
|
+
* the same question asked again and blanking it flashes an empty list.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useFetch<Data>(key: FetchKey, fetcher: () => Promise<Data>, { onSuccess }?: {
|
|
14
|
+
onSuccess?: (data: Data) => void;
|
|
15
|
+
}): {
|
|
16
|
+
data: Data | undefined;
|
|
17
|
+
error: unknown;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
mutate: () => void;
|
|
20
|
+
};
|
|
21
|
+
/** `value`, but only after it has stopped changing for `ms`. */
|
|
22
|
+
export declare function useDebounced<T>(value: T, ms: number): T;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
const isNil = (k) => k === null || k === undefined || k === false;
|
|
3
|
+
// A null result means "don't fetch": no key, or an array key with a missing
|
|
4
|
+
// piece. Note what that makes of a boolean IN a key -- `false` reads as a
|
|
5
|
+
// missing piece and silently disables the fetch, so put a string there.
|
|
6
|
+
export function serializeKey(key) {
|
|
7
|
+
return isNil(key) || (Array.isArray(key) && key.some(isNil))
|
|
8
|
+
? null
|
|
9
|
+
: JSON.stringify(key);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fetch once per key, keeping loading and error state with the data instead of
|
|
13
|
+
* spread across three `useState`s and a cancellation flag.
|
|
14
|
+
*
|
|
15
|
+
* Everything the caller reads is derived from the one settled result, which is
|
|
16
|
+
* what keeps the states consistent: while a key change is in flight there is no
|
|
17
|
+
* frame showing the previous key's data as though it were the new key's. A
|
|
18
|
+
* `mutate()` refetch under the same key does leave its data up, because it is
|
|
19
|
+
* the same question asked again and blanking it flashes an empty list.
|
|
20
|
+
*/
|
|
21
|
+
export function useFetch(key, fetcher, { onSuccess } = {}) {
|
|
22
|
+
const serialized = serializeKey(key);
|
|
23
|
+
const [nonce, setNonce] = useState(0);
|
|
24
|
+
const [settled, setSettled] = useState();
|
|
25
|
+
const attempt = serialized === null ? null : `${nonce}\u0000${serialized}`;
|
|
26
|
+
const current = settled?.attempt === attempt;
|
|
27
|
+
const answersThisKey = settled?.question === serialized;
|
|
28
|
+
// The fetcher and the success callback are inline closures at every call site,
|
|
29
|
+
// so depending on their identity would refetch every render. This effect is
|
|
30
|
+
// declared before the fetch effect and has no dependency list, so it has
|
|
31
|
+
// already run with the current render's values by the time that one does.
|
|
32
|
+
const latest = useRef({ fetcher, onSuccess });
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
latest.current = { fetcher, onSuccess };
|
|
35
|
+
});
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (attempt === null || serialized === null) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
let alive = true;
|
|
41
|
+
latest.current
|
|
42
|
+
.fetcher()
|
|
43
|
+
.then(data => {
|
|
44
|
+
if (alive) {
|
|
45
|
+
setSettled({ attempt, question: serialized, data });
|
|
46
|
+
latest.current.onSuccess?.(data);
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.catch((error) => {
|
|
50
|
+
if (alive) {
|
|
51
|
+
setSettled({ attempt, question: serialized, error });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
alive = false;
|
|
56
|
+
};
|
|
57
|
+
}, [attempt, serialized]);
|
|
58
|
+
return {
|
|
59
|
+
data: answersThisKey ? settled.data : undefined,
|
|
60
|
+
error: current ? settled.error : undefined,
|
|
61
|
+
isLoading: attempt !== null && !current,
|
|
62
|
+
mutate: useCallback(() => {
|
|
63
|
+
setNonce(n => n + 1);
|
|
64
|
+
}, []),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** `value`, but only after it has stopped changing for `ms`. */
|
|
68
|
+
export function useDebounced(value, ms) {
|
|
69
|
+
const [debounced, setDebounced] = useState(value);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const timer = setTimeout(() => {
|
|
72
|
+
setDebounced(value);
|
|
73
|
+
}, ms);
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
};
|
|
77
|
+
}, [value, ms]);
|
|
78
|
+
return debounced;
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { serializeKey } from './useFetch';
|
|
3
|
+
describe('serializeKey', () => {
|
|
4
|
+
test('a key identifies the fetch', () => {
|
|
5
|
+
expect(serializeKey('msa-list')).toBe(serializeKey('msa-list'));
|
|
6
|
+
expect(serializeKey(['NM_1', 'hg38'])).not.toBe(serializeKey(['NM_1', 'mm39']));
|
|
7
|
+
});
|
|
8
|
+
test('no key means do not fetch', () => {
|
|
9
|
+
expect(serializeKey(null)).toBeNull();
|
|
10
|
+
expect(serializeKey(undefined)).toBeNull();
|
|
11
|
+
expect(serializeKey(false)).toBeNull();
|
|
12
|
+
expect(serializeKey('')).not.toBeNull();
|
|
13
|
+
});
|
|
14
|
+
test('an array key with a piece still missing means do not fetch', () => {
|
|
15
|
+
expect(serializeKey(['NM_1', undefined, 'feature-sequence'])).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
// the trap this rule sets: a boolean IN a key reads as a missing piece, so a
|
|
18
|
+
// `!!selected` where a string belonged disables the fetch and raises nothing
|
|
19
|
+
test('a boolean in a key disables the fetch', () => {
|
|
20
|
+
expect(serializeKey(['gene', false])).toBeNull();
|
|
21
|
+
expect(serializeKey(['gene', true])).not.toBeNull();
|
|
22
|
+
});
|
|
23
|
+
});
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "2.10.
|
|
1
|
+
export declare const version = "2.10.2";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.10.
|
|
1
|
+
export const version = '2.10.2';
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.10.
|
|
2
|
+
"version": "2.10.2",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"name": "jbrowse-plugin-msaview",
|
|
5
5
|
"repository": {
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
"@emotion/styled": "^11.14.1",
|
|
20
20
|
"@gmod/bgzf-filehandle": "^6.2.0",
|
|
21
21
|
"g2p_mapper": "^2.1.5",
|
|
22
|
-
"idb": "^8.0.3"
|
|
23
|
-
"swr": "^2.4.2"
|
|
22
|
+
"idb": "^8.0.3"
|
|
24
23
|
},
|
|
25
24
|
"devDependencies": {
|
|
26
25
|
"@emotion/react": "^11.14.0",
|
|
@@ -41,9 +40,10 @@
|
|
|
41
40
|
"eslint-plugin-react": "^7.37.5",
|
|
42
41
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
43
42
|
"eslint-plugin-unicorn": "^72.0.0",
|
|
43
|
+
"git-cliff": "^2.13.1",
|
|
44
44
|
"mobx": "^6.16.1",
|
|
45
45
|
"mobx-react": "^9.2.2",
|
|
46
|
-
"msa-parsers": "^5.
|
|
46
|
+
"msa-parsers": "^5.10.0",
|
|
47
47
|
"pixelmatch": "^7.2.0",
|
|
48
48
|
"pngjs": "^7.0.0",
|
|
49
49
|
"prettier": "^3.9.6",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"puppeteer": "^25.3.0",
|
|
52
52
|
"react": "^19.2.8",
|
|
53
53
|
"react-dom": "^19.2.8",
|
|
54
|
-
"react-msaview": "^5.
|
|
54
|
+
"react-msaview": "^5.10.0",
|
|
55
55
|
"rimraf": "^6.1.3",
|
|
56
56
|
"rxjs": "^7.8.2",
|
|
57
57
|
"serve": "^14.2.6",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"build": "tsc && NODE_ENV=production node esbuild.mjs && cp distconfig.json dist/config.json",
|
|
68
68
|
"prebuild": "pnpm clean",
|
|
69
69
|
"lint": "eslint src --report-unused-disable-directives --max-warnings 0",
|
|
70
|
+
"check-text-source": "node scripts/check-text-source.mjs",
|
|
70
71
|
"pretest": "rm -rf .test-jbrowse && npx @jbrowse/cli create .test-jbrowse --nightly",
|
|
71
72
|
"test": "vitest run",
|
|
72
73
|
"test:watch": "vitest",
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
"host-compat": "node scripts/host-compat-probe.mjs --bundle dist/jbrowse-plugin-msaview.umd.production.min.js",
|
|
78
79
|
"check-ci": "node scripts/require-green-ci.mjs",
|
|
79
80
|
"preversion": "pnpm check-ci && pnpm lint && pnpm build && pnpm host-compat",
|
|
80
|
-
"version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git add src/version.ts",
|
|
81
|
+
"version": "node -e \"console.log('export const version = \\'' + require('./package.json').version + '\\'')\" > src/version.ts && git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add src/version.ts CHANGELOG.md",
|
|
81
82
|
"postversion": "git push --follow-tags"
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import useSWR from 'swr'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
deleteCachedResult,
|
|
5
3
|
getAllCachedResults,
|
|
6
4
|
} from '../../../utils/blastCache'
|
|
7
|
-
import {
|
|
5
|
+
import { useFetch } from '../../../utils/useFetch'
|
|
8
6
|
|
|
9
7
|
export function useCachedBlastResults(geneIds: string[]) {
|
|
10
8
|
const {
|
|
@@ -12,36 +10,25 @@ export function useCachedBlastResults(geneIds: string[]) {
|
|
|
12
10
|
error,
|
|
13
11
|
isLoading,
|
|
14
12
|
mutate,
|
|
15
|
-
} =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return cached.filter(r => r.geneId && geneIds.includes(r.geneId))
|
|
20
|
-
},
|
|
21
|
-
staticSwrConfig,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const handleDelete = async (id: string) => {
|
|
25
|
-
await deleteCachedResult(id)
|
|
26
|
-
await mutate(
|
|
27
|
-
results => results?.filter(result => result.id !== id) ?? [],
|
|
28
|
-
false,
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// deletes only what this hook listed, i.e. the results for these gene ids.
|
|
33
|
-
// The list the user is looking at is gene-scoped, so a store-wide clear here
|
|
34
|
-
// would silently throw away every other gene's cached alignments too
|
|
35
|
-
const handleClearAll = async () => {
|
|
36
|
-
await Promise.all((results ?? []).map(r => deleteCachedResult(r.id)))
|
|
37
|
-
await mutate([], false)
|
|
38
|
-
}
|
|
13
|
+
} = useFetch(`cached-blast-${geneIds.join(',')}`, async () => {
|
|
14
|
+
const cached = await getAllCachedResults()
|
|
15
|
+
return cached.filter(r => r.geneId && geneIds.includes(r.geneId))
|
|
16
|
+
})
|
|
39
17
|
|
|
40
18
|
return {
|
|
41
19
|
results: results ?? [],
|
|
42
20
|
error,
|
|
43
21
|
isLoading,
|
|
44
|
-
handleDelete
|
|
45
|
-
|
|
22
|
+
handleDelete: async (id: string) => {
|
|
23
|
+
await deleteCachedResult(id)
|
|
24
|
+
mutate()
|
|
25
|
+
},
|
|
26
|
+
// deletes only what this hook listed, i.e. the results for these gene ids.
|
|
27
|
+
// The list the user is looking at is gene-scoped, so a store-wide clear here
|
|
28
|
+
// would silently throw away every other gene's cached alignments too
|
|
29
|
+
handleClearAll: async () => {
|
|
30
|
+
await Promise.all((results ?? []).map(r => deleteCachedResult(r.id)))
|
|
31
|
+
mutate()
|
|
32
|
+
},
|
|
46
33
|
}
|
|
47
34
|
}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import TextField2 from '../../../components/TextField2'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveAssemblySpecies,
|
|
6
|
+
resolveTaxId,
|
|
7
|
+
} from '../../../utils/ncbiTaxonomy'
|
|
5
8
|
import { fetchTaxonomyInfo } from '../../../utils/taxonomyNames'
|
|
9
|
+
import { useDebounced, useFetch } from '../../../utils/useFetch'
|
|
10
|
+
|
|
11
|
+
async function describeTaxon(query: string) {
|
|
12
|
+
const taxId = await resolveTaxId(query)
|
|
13
|
+
if (!taxId) {
|
|
14
|
+
throw new Error(`No NCBI taxon matches "${query}"`)
|
|
15
|
+
}
|
|
16
|
+
const info = (await fetchTaxonomyInfo([taxId])).get(taxId)
|
|
17
|
+
const label = [info?.sciname, info?.commonName && `(${info.commonName})`]
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join(' ')
|
|
20
|
+
return { taxId, label: label || `taxon ${taxId}` }
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
/**
|
|
8
24
|
* The species the query gene came from, as free text resolved against NCBI's
|
|
@@ -15,62 +31,60 @@ import { fetchTaxonomyInfo } from '../../../utils/taxonomyNames'
|
|
|
15
31
|
*/
|
|
16
32
|
export default function QuerySpeciesSelect({
|
|
17
33
|
value,
|
|
34
|
+
assemblyName,
|
|
18
35
|
onChange,
|
|
19
36
|
className,
|
|
20
37
|
}: {
|
|
21
38
|
value: number
|
|
39
|
+
assemblyName?: string
|
|
22
40
|
onChange: (taxId: number) => void
|
|
23
41
|
className?: string
|
|
24
42
|
}) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const [
|
|
43
|
+
// undefined until the user types, which is what makes the two lookups
|
|
44
|
+
// exclusive rather than both firing on open
|
|
45
|
+
const [typed, setTyped] = useState<string>()
|
|
46
|
+
const debounced = useDebounced(typed, 400)
|
|
28
47
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
// Opening on `human` for everyone is the same silent wrong answer the fixed
|
|
49
|
+
// species list gave: on a mouse assembly the gene symbol resolves to the HUMAN
|
|
50
|
+
// gene, and the excluded taxon is human too, so mouse appears twice. The
|
|
51
|
+
// assembly being browsed is the one thing here that already knows the answer.
|
|
52
|
+
//
|
|
53
|
+
// db=assembly already returns the taxon id, so this is the whole lookup — the
|
|
54
|
+
// taxonomy chain below would be two more requests for an answer we hold. That
|
|
55
|
+
// is not just waste: eutils allows 3 requests a second and throttles by
|
|
56
|
+
// answering without CORS headers, so all four fired on open and the browser
|
|
57
|
+
// reported the throttle as "blocked by CORS policy" in the helper text.
|
|
58
|
+
const { data: fromAssembly } = useFetch(
|
|
59
|
+
assemblyName && typed === undefined
|
|
60
|
+
? [assemblyName, 'assembly-species']
|
|
61
|
+
: null,
|
|
62
|
+
() => resolveAssemblySpecies(assemblyName!),
|
|
63
|
+
{
|
|
64
|
+
onSuccess: found => {
|
|
65
|
+
if (found) {
|
|
66
|
+
onChange(found.taxId)
|
|
46
67
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const { data: fromText, error } = useFetch(
|
|
73
|
+
debounced?.trim() ? [debounced.trim(), 'taxon'] : null,
|
|
74
|
+
() => describeTaxon(debounced!),
|
|
75
|
+
{
|
|
76
|
+
onSuccess: ({ taxId }) => {
|
|
56
77
|
onChange(taxId)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
run.live = false
|
|
68
|
-
clearTimeout(timer)
|
|
69
|
-
}
|
|
70
|
-
// onChange is a setState updater from the parent and stable in practice;
|
|
71
|
-
// including it would re-run the lookup on every parent render
|
|
72
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
-
}, [text])
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// derived, not seeded through an effect: whatever the user typed wins, and
|
|
83
|
+
// until they type anything the assembly's species does, so a lookup that
|
|
84
|
+
// lands while they are mid-word cannot overwrite the field
|
|
85
|
+
const text = typed ?? fromAssembly?.speciesName ?? 'human'
|
|
86
|
+
const resolved =
|
|
87
|
+
typed === undefined ? fromAssembly?.speciesName : fromText?.label
|
|
74
88
|
|
|
75
89
|
return (
|
|
76
90
|
<TextField2
|
|
@@ -79,7 +93,7 @@ export default function QuerySpeciesSelect({
|
|
|
79
93
|
className={className}
|
|
80
94
|
value={text}
|
|
81
95
|
onChange={event => {
|
|
82
|
-
|
|
96
|
+
setTyped(event.target.value)
|
|
83
97
|
}}
|
|
84
98
|
error={!!error}
|
|
85
99
|
helperText={
|
|
@@ -4,11 +4,10 @@ import { LoadingEllipses, SanitizedHTML } from '@jbrowse/core/ui'
|
|
|
4
4
|
import { getEnv, getSession } from '@jbrowse/core/util'
|
|
5
5
|
import { MenuItem } from '@mui/material'
|
|
6
6
|
import { observer } from 'mobx-react'
|
|
7
|
-
import useSWR from 'swr'
|
|
8
7
|
import { makeStyles } from 'tss-react/mui'
|
|
9
8
|
|
|
10
9
|
import TextField2 from '../../../components/TextField2'
|
|
11
|
-
import {
|
|
10
|
+
import { useFetch } from '../../../utils/useFetch'
|
|
12
11
|
import { getGeneDisplayName, getLinearGenomeView } from '../../util'
|
|
13
12
|
import LaunchPanelContent from '../LaunchPanelContent'
|
|
14
13
|
import SubmitCancelActions from '../SubmitCancelActions'
|
|
@@ -51,10 +50,9 @@ const PreLoadedMSA = observer(function ({
|
|
|
51
50
|
data: msaList,
|
|
52
51
|
isLoading: msaListLoading,
|
|
53
52
|
error: msaListFetchError,
|
|
54
|
-
} =
|
|
53
|
+
} = useFetch(
|
|
55
54
|
selectedDataset ? `${selectedDataset.datasetId}-msa-list` : null,
|
|
56
55
|
() => fetchMSAList({ config: selectedDataset!.adapter, pluginManager }),
|
|
57
|
-
staticSwrConfig,
|
|
58
56
|
)
|
|
59
57
|
|
|
60
58
|
const transcriptSelection = useTranscriptSelection({
|
|
@@ -68,7 +66,7 @@ const PreLoadedMSA = observer(function ({
|
|
|
68
66
|
data: msaData,
|
|
69
67
|
isLoading: msaDataLoading,
|
|
70
68
|
error: msaDataFetchError,
|
|
71
|
-
} =
|
|
69
|
+
} = useFetch(
|
|
72
70
|
selectedId && selectedDataset && msaList
|
|
73
71
|
? `${selectedDataset.datasetId}-${selectedId}-msa`
|
|
74
72
|
: null,
|
|
@@ -78,7 +76,6 @@ const PreLoadedMSA = observer(function ({
|
|
|
78
76
|
config: selectedDataset!.adapter,
|
|
79
77
|
pluginManager,
|
|
80
78
|
}),
|
|
81
|
-
staticSwrConfig,
|
|
82
79
|
)
|
|
83
80
|
|
|
84
81
|
const e =
|