@jbrowse/core 4.0.3 → 4.1.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/esm/BaseFeatureWidget/BaseFeatureDetail/ArrayValue.js +10 -1
- package/esm/PluginLoader.js +11 -9
- package/esm/pluggableElementTypes/RpcMethodType.d.ts +1 -0
- package/esm/pluggableElementTypes/RpcMethodType.js +49 -5
- package/esm/pluggableElementTypes/RpcMethodTypeWithFiltersAndRenameRegions.js +1 -0
- package/esm/pluggableElementTypes/models/BaseDisplayModel.d.ts +3 -0
- package/esm/pluggableElementTypes/models/BaseDisplayModel.js +4 -1
- package/esm/pluggableElementTypes/models/saveTrackFileTypes/bed.js +5 -2
- package/esm/pluggableElementTypes/models/saveTrackFileTypes/genbank.js +57 -26
- package/esm/pluggableElementTypes/models/saveTrackFileTypes/gff3.js +35 -16
- package/esm/pluggableElementTypes/renderers/LayoutSession.js +6 -3
- package/esm/pluggableElementTypes/renderers/ServerSideRendererType.js +4 -1
- package/esm/pluggableElementTypes/renderers/util/serializableFilterChain.d.ts +2 -1
- package/esm/pluggableElementTypes/renderers/util/serializableFilterChain.js +2 -2
- package/esm/rpc/methods/CoreRender.js +7 -2
- package/esm/ui/CascadingMenu.js +10 -2
- package/esm/ui/FileSelector/FileSelector.d.ts +0 -1
- package/esm/ui/FileSelector/FileSelector.js +19 -31
- package/esm/ui/FileSelector/LocalFileChooser.js +90 -26
- package/esm/ui/FileSelector/SourceTypeSelector.js +18 -33
- package/esm/ui/FileSelector/util.d.ts +8 -0
- package/esm/ui/FileSelector/util.js +34 -0
- package/esm/ui/HoverMenu.d.ts +1 -1
- package/esm/ui/SnackbarContents.js +4 -4
- package/esm/ui/SnackbarModel.d.ts +4 -4
- package/esm/ui/SnackbarModel.js +18 -7
- package/esm/ui/index.d.ts +1 -1
- package/esm/ui/index.js +1 -1
- package/esm/util/IntervalTree.d.ts +42 -0
- package/esm/util/IntervalTree.js +257 -0
- package/esm/util/colord.d.ts +22 -8
- package/esm/util/colord.js +227 -10
- package/esm/util/crypto.d.ts +7 -0
- package/esm/util/crypto.js +536 -0
- package/esm/util/fileHandleStore.d.ts +6 -0
- package/esm/util/fileHandleStore.js +68 -0
- package/esm/util/idMaker.js +9 -1
- package/esm/util/index.d.ts +3 -0
- package/esm/util/index.js +3 -0
- package/esm/util/io/index.js +11 -1
- package/esm/util/jexl.js +1 -0
- package/esm/util/tracks.d.ts +41 -7
- package/esm/util/tracks.js +141 -9
- package/esm/util/types/ElementId.d.ts +1 -0
- package/esm/util/types/ElementId.js +4 -1
- package/esm/util/types/index.d.ts +11 -4
- package/esm/util/types/index.js +6 -0
- package/esm/util/types/mst.d.ts +18 -1
- package/esm/util/types/mst.js +11 -3
- package/package.json +356 -21
- package/esm/ui/FileSelector/index.d.ts +0 -1
- package/esm/ui/FileSelector/index.js +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import Attributes from "./Attributes.js";
|
|
3
4
|
import BasicValue from "./BasicValue.js";
|
|
4
5
|
import FieldName from "./FieldName.js";
|
|
5
6
|
import { isObject } from "../../util/index.js";
|
|
6
7
|
import { makeStyles } from "../../util/tss-react/index.js";
|
|
8
|
+
const MAX_ARRAY_LENGTH = 100;
|
|
7
9
|
const useStyles = makeStyles()(theme => ({
|
|
8
10
|
field: {
|
|
9
11
|
display: 'flex',
|
|
@@ -20,6 +22,9 @@ const useStyles = makeStyles()(theme => ({
|
|
|
20
22
|
}));
|
|
21
23
|
export default function ArrayValue({ name, value, description, formatter, prefix = [], }) {
|
|
22
24
|
const { classes } = useStyles();
|
|
25
|
+
const [showAll, setShowAll] = useState(false);
|
|
26
|
+
const needsTruncation = value.length > MAX_ARRAY_LENGTH;
|
|
27
|
+
const displayedValues = needsTruncation && !showAll ? value.slice(0, MAX_ARRAY_LENGTH) : value;
|
|
23
28
|
if (value.length === 1) {
|
|
24
29
|
return isObject(value[0]) ? (_jsx(Attributes, { formatter: formatter, attributes: value[0], prefix: [...prefix, name] })) : (_jsxs("div", { className: classes.field, children: [_jsx(FieldName, { prefix: prefix, description: description, name: name }), _jsx(BasicValue, { value: formatter ? formatter(value[0], name) : value[0] })] }));
|
|
25
30
|
}
|
|
@@ -27,6 +32,10 @@ export default function ArrayValue({ name, value, description, formatter, prefix
|
|
|
27
32
|
return (_jsx(_Fragment, { children: value.map((val, i) => (_jsx(Attributes, { formatter: formatter, attributes: val, prefix: [...prefix, `${name}-${i}`] }, `${JSON.stringify(val)}-${i}`))) }));
|
|
28
33
|
}
|
|
29
34
|
else {
|
|
30
|
-
return (_jsxs("div", { className: classes.field, children: [_jsx(FieldName, { prefix: prefix, description: description, name: name }),
|
|
35
|
+
return (_jsxs("div", { className: classes.field, children: [_jsx(FieldName, { prefix: prefix, description: description, name: name }), displayedValues.map((val, i) => (_jsx("div", { className: classes.fieldSubvalue, children: _jsx(BasicValue, { value: formatter ? formatter(val, name) : val }) }, `${JSON.stringify(val)}-${i}`))), needsTruncation ? (_jsx("button", { type: "button", onClick: () => {
|
|
36
|
+
setShowAll(val => !val);
|
|
37
|
+
}, children: showAll
|
|
38
|
+
? 'Show less'
|
|
39
|
+
: `Showing ${MAX_ARRAY_LENGTH} of ${value.length}. Show all...` })) : null] }));
|
|
31
40
|
}
|
|
32
41
|
}
|
package/esm/PluginLoader.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import domLoadScript from 'load-script';
|
|
2
1
|
import Plugin from "./Plugin.js";
|
|
3
2
|
import ReExports from "./ReExports/index.js";
|
|
4
3
|
import { isElectron } from "./util/index.js";
|
|
@@ -14,14 +13,17 @@ export function isESMPluginDefinition(def) {
|
|
|
14
13
|
}
|
|
15
14
|
function promisifiedLoadScript(src) {
|
|
16
15
|
return new Promise((resolve, reject) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const script = document.createElement('script');
|
|
17
|
+
script.type = 'text/javascript';
|
|
18
|
+
script.async = true;
|
|
19
|
+
script.src = src;
|
|
20
|
+
script.onload = () => {
|
|
21
|
+
resolve(script.src);
|
|
22
|
+
};
|
|
23
|
+
script.onerror = () => {
|
|
24
|
+
reject(new Error(`Failed to load script: ${src}`));
|
|
25
|
+
};
|
|
26
|
+
document.head.append(script);
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
async function loadScript(scriptUrl) {
|
|
@@ -2,6 +2,7 @@ import PluggableElementBase from './PluggableElementBase.ts';
|
|
|
2
2
|
import type PluginManager from '../PluginManager.ts';
|
|
3
3
|
import type { UriLocation } from '../util/types/index.ts';
|
|
4
4
|
export type RpcMethodConstructor = new (pm: PluginManager) => RpcMethodType;
|
|
5
|
+
export declare function convertFileHandleLocations(obj: unknown, blobMap: Record<string, File>, seen?: WeakSet<object>): void;
|
|
5
6
|
export default abstract class RpcMethodType extends PluggableElementBase {
|
|
6
7
|
pluginManager: PluginManager;
|
|
7
8
|
constructor(pluginManager: PluginManager);
|
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import PluggableElementBase from "./PluggableElementBase.js";
|
|
2
2
|
import mapObject from "../util/map-obj/index.js";
|
|
3
3
|
import { isRpcResult } from "../util/rpc.js";
|
|
4
|
-
import { getBlobMap, setBlobMap } from "../util/tracks.js";
|
|
5
|
-
import { RetryError, isAppRootModel, isAuthNeededException, isUriLocation, } from "../util/types/index.js";
|
|
4
|
+
import { getBlobMap, getFileFromCache, setBlobMap } from "../util/tracks.js";
|
|
5
|
+
import { RetryError, isAppRootModel, isAuthNeededException, isFileHandleLocation, isUriLocation, } from "../util/types/index.js";
|
|
6
|
+
export function convertFileHandleLocations(obj, blobMap, seen = new WeakSet()) {
|
|
7
|
+
const convertLocation = (loc) => {
|
|
8
|
+
const file = getFileFromCache(loc.handleId);
|
|
9
|
+
if (!file) {
|
|
10
|
+
throw new Error(`File not in cache for handleId: ${loc.handleId}. ` +
|
|
11
|
+
`The file "${loc.name}" may need to be reopened.`);
|
|
12
|
+
}
|
|
13
|
+
const blobId = `fh-blob-${loc.handleId}`;
|
|
14
|
+
blobMap[blobId] = file;
|
|
15
|
+
return { locationType: 'BlobLocation', name: loc.name, blobId };
|
|
16
|
+
};
|
|
17
|
+
const convert = (current) => {
|
|
18
|
+
if (!current || typeof current !== 'object' || seen.has(current)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
seen.add(current);
|
|
22
|
+
if (Array.isArray(current)) {
|
|
23
|
+
for (let i = 0; i < current.length; i++) {
|
|
24
|
+
const item = current[i];
|
|
25
|
+
if (isFileHandleLocation(item)) {
|
|
26
|
+
current[i] = convertLocation(item);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
convert(item);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const record = current;
|
|
35
|
+
for (const key of Object.keys(record)) {
|
|
36
|
+
const val = record[key];
|
|
37
|
+
if (isFileHandleLocation(val)) {
|
|
38
|
+
record[key] = convertLocation(val);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
convert(val);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
convert(obj);
|
|
47
|
+
}
|
|
6
48
|
export default class RpcMethodType extends PluggableElementBase {
|
|
7
49
|
pluginManager;
|
|
8
50
|
constructor(pluginManager) {
|
|
@@ -62,11 +104,10 @@ export default class RpcMethodType extends PluggableElementBase {
|
|
|
62
104
|
}
|
|
63
105
|
async augmentLocationObjects(thing, rpcDriverClassName) {
|
|
64
106
|
const rootModel = this.pluginManager.rootModel;
|
|
65
|
-
if (isAppRootModel(rootModel) && rootModel.internetAccounts.length === 0) {
|
|
66
|
-
return thing;
|
|
67
|
-
}
|
|
68
107
|
const uris = [];
|
|
108
|
+
const blobMap = getBlobMap();
|
|
69
109
|
const { renderingProps, ...rest } = thing;
|
|
110
|
+
convertFileHandleLocations(rest, blobMap);
|
|
70
111
|
mapObject(rest, (key, val) => {
|
|
71
112
|
if (isUriLocation(val)) {
|
|
72
113
|
uris.push(val);
|
|
@@ -80,6 +121,9 @@ export default class RpcMethodType extends PluggableElementBase {
|
|
|
80
121
|
}
|
|
81
122
|
return [key, val];
|
|
82
123
|
}, { deep: true });
|
|
124
|
+
if (isAppRootModel(rootModel) && rootModel.internetAccounts.length === 0) {
|
|
125
|
+
return thing;
|
|
126
|
+
}
|
|
83
127
|
for (const uri of uris) {
|
|
84
128
|
await this.serializeNewAuthArguments(uri, rpcDriverClassName);
|
|
85
129
|
}
|
|
@@ -52,6 +52,7 @@ export declare const BaseDisplay: import("@jbrowse/mobx-state-tree").IModelType<
|
|
|
52
52
|
}> | null;
|
|
53
53
|
readonly adapterConfig: any;
|
|
54
54
|
readonly parentTrack: import("../../util/index.ts").AbstractTrackModel;
|
|
55
|
+
readonly isMinimized: boolean;
|
|
55
56
|
readonly parentDisplay: any;
|
|
56
57
|
readonly effectiveRpcDriverName: any;
|
|
57
58
|
} & {
|
|
@@ -108,6 +109,7 @@ export declare const BaseDisplay: import("@jbrowse/mobx-state-tree").IModelType<
|
|
|
108
109
|
}> | null;
|
|
109
110
|
readonly adapterConfig: any;
|
|
110
111
|
readonly parentTrack: import("../../util/index.ts").AbstractTrackModel;
|
|
112
|
+
readonly isMinimized: boolean;
|
|
111
113
|
readonly parentDisplay: any;
|
|
112
114
|
readonly effectiveRpcDriverName: any;
|
|
113
115
|
} & import("@jbrowse/mobx-state-tree").IStateTreeNode<import("@jbrowse/mobx-state-tree").IModelType<{
|
|
@@ -161,6 +163,7 @@ export declare const BaseDisplay: import("@jbrowse/mobx-state-tree").IModelType<
|
|
|
161
163
|
}> | null;
|
|
162
164
|
readonly adapterConfig: any;
|
|
163
165
|
readonly parentTrack: import("../../util/index.ts").AbstractTrackModel;
|
|
166
|
+
readonly isMinimized: boolean;
|
|
164
167
|
readonly parentDisplay: any;
|
|
165
168
|
readonly effectiveRpcDriverName: any;
|
|
166
169
|
}, import("@jbrowse/mobx-state-tree")._NotCustomized, import("@jbrowse/mobx-state-tree")._NotCustomized>>;
|
|
@@ -30,6 +30,9 @@ function stateModelFactory() {
|
|
|
30
30
|
get parentTrack() {
|
|
31
31
|
return getContainingTrack(self);
|
|
32
32
|
},
|
|
33
|
+
get isMinimized() {
|
|
34
|
+
return this.parentTrack.minimized;
|
|
35
|
+
},
|
|
33
36
|
get parentDisplay() {
|
|
34
37
|
try {
|
|
35
38
|
const parent = getParent(self);
|
|
@@ -62,7 +65,7 @@ function stateModelFactory() {
|
|
|
62
65
|
renderProps() {
|
|
63
66
|
return {
|
|
64
67
|
...getParentRenderProps(self),
|
|
65
|
-
notReady: getContainingView(self).minimized,
|
|
68
|
+
notReady: self.isMinimized || getContainingView(self).minimized,
|
|
66
69
|
rpcDriverName: self.effectiveRpcDriverName,
|
|
67
70
|
};
|
|
68
71
|
},
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export function stringifyBED({ features }) {
|
|
2
|
+
if (features.length === 0) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
2
5
|
const fields = ['refName', 'start', 'end', 'name', 'score', 'strand'];
|
|
3
|
-
return features
|
|
6
|
+
return `${features
|
|
4
7
|
.map(feature => fields
|
|
5
8
|
.map(field => feature.get(field) ?? '.')
|
|
6
9
|
.join('\t')
|
|
7
10
|
.trim())
|
|
8
|
-
.join('\n')
|
|
11
|
+
.join('\n')}\n`;
|
|
9
12
|
}
|
|
@@ -2,6 +2,7 @@ import { fetchSequence } from "./fetchSequence.js";
|
|
|
2
2
|
import { max, min } from "../../../util/index.js";
|
|
3
3
|
const coreFields = new Set([
|
|
4
4
|
'uniqueId',
|
|
5
|
+
'id',
|
|
5
6
|
'refName',
|
|
6
7
|
'source',
|
|
7
8
|
'type',
|
|
@@ -14,31 +15,42 @@ const coreFields = new Set([
|
|
|
14
15
|
'subfeatures',
|
|
15
16
|
'phase',
|
|
16
17
|
]);
|
|
18
|
+
const TYPE_COLUMN_WIDTH = 16;
|
|
17
19
|
const blank = ' ';
|
|
18
20
|
const retitle = {
|
|
19
21
|
name: 'Name',
|
|
20
22
|
};
|
|
21
23
|
function fmt(obj) {
|
|
24
|
+
if (obj === null || obj === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
22
27
|
if (Array.isArray(obj)) {
|
|
23
|
-
|
|
28
|
+
const items = obj.map(o => fmt(o)).filter(o => o !== undefined);
|
|
29
|
+
return items.length > 0 ? items.join(',') : undefined;
|
|
24
30
|
}
|
|
25
|
-
|
|
31
|
+
if (typeof obj === 'object') {
|
|
26
32
|
return JSON.stringify(obj);
|
|
27
33
|
}
|
|
28
|
-
|
|
29
|
-
return `${obj}`;
|
|
30
|
-
}
|
|
34
|
+
return `${obj}`;
|
|
31
35
|
}
|
|
32
36
|
function formatTags(f, parentId, parentType) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
const tags = [];
|
|
38
|
+
if (parentId && parentType) {
|
|
39
|
+
tags.push(`${blank}/${parentType}="${parentId}"`);
|
|
40
|
+
}
|
|
41
|
+
const id = f.get('id');
|
|
42
|
+
if (id) {
|
|
43
|
+
tags.push(`${blank}/name="${id}"`);
|
|
44
|
+
}
|
|
45
|
+
for (const key of Object.keys(f.toJSON())) {
|
|
46
|
+
if (!coreFields.has(key) && key !== parentType) {
|
|
47
|
+
const value = fmt(f.get(key));
|
|
48
|
+
if (value) {
|
|
49
|
+
tags.push(`${blank}/${retitle[key] || key}="${value}"`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return tags;
|
|
42
54
|
}
|
|
43
55
|
function relativeStart(f, min) {
|
|
44
56
|
return f.get('start') - min + 1;
|
|
@@ -50,36 +62,56 @@ function loc(f, min) {
|
|
|
50
62
|
return `${relativeStart(f, min)}..${relativeEnd(f, min)}`;
|
|
51
63
|
}
|
|
52
64
|
function formatFeat(f, min, parentType, parentId) {
|
|
53
|
-
const type = `${f.get('type')}`.slice(0,
|
|
65
|
+
const type = `${f.get('type')}`.slice(0, TYPE_COLUMN_WIDTH);
|
|
54
66
|
const l = loc(f, min);
|
|
55
67
|
const locstrand = f.get('strand') === -1 ? `complement(${l})` : l;
|
|
56
68
|
return [
|
|
57
|
-
` ${type.padEnd(
|
|
69
|
+
` ${type.padEnd(TYPE_COLUMN_WIDTH)}${locstrand}`,
|
|
58
70
|
...formatTags(f, parentType, parentId),
|
|
59
71
|
];
|
|
60
72
|
}
|
|
61
73
|
function formatCDS(feats, parentId, parentType, strand, min) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
if (feats.length === 0) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const locs = feats.map(f => loc(f, min));
|
|
78
|
+
const locExpr = locs.length === 1 ? locs[0] : `join(${locs.join(',')})`;
|
|
79
|
+
const strandExpr = strand === -1 ? `complement(${locExpr})` : locExpr;
|
|
80
|
+
return [
|
|
81
|
+
` ${'CDS'.padEnd(TYPE_COLUMN_WIDTH)}${strandExpr}`,
|
|
82
|
+
`${blank}/${parentType}="${parentId}"`,
|
|
83
|
+
];
|
|
68
84
|
}
|
|
69
85
|
export function formatFeatWithSubfeatures(feature, min, parentId, parentType) {
|
|
70
86
|
const primary = formatFeat(feature, min, parentId, parentType);
|
|
71
87
|
const subfeatures = feature.get('subfeatures') || [];
|
|
72
|
-
const cds = subfeatures
|
|
88
|
+
const cds = subfeatures
|
|
89
|
+
.filter(f => f.get('type') === 'CDS')
|
|
90
|
+
.sort((a, b) => a.get('start') - b.get('start'));
|
|
73
91
|
const sansCDS = subfeatures.filter(f => f.get('type') !== 'CDS' && f.get('type') !== 'exon');
|
|
74
92
|
const newParentId = feature.get('id');
|
|
75
93
|
const newParentType = feature.get('type');
|
|
76
94
|
const newParentStrand = feature.get('strand');
|
|
95
|
+
const cdsLines = cds.length > 0 && newParentId && newParentType
|
|
96
|
+
? formatCDS(cds, newParentId, newParentType, newParentStrand, min)
|
|
97
|
+
: [];
|
|
77
98
|
return [
|
|
78
99
|
...primary,
|
|
79
|
-
...
|
|
100
|
+
...cdsLines,
|
|
80
101
|
...sansCDS.flatMap(sub => formatFeatWithSubfeatures(sub, min, newParentId, newParentType)),
|
|
81
102
|
].join('\n');
|
|
82
103
|
}
|
|
104
|
+
function formatOrigin(sequence) {
|
|
105
|
+
const lines = ['ORIGIN'];
|
|
106
|
+
for (let i = 0; i < sequence.length; i += 60) {
|
|
107
|
+
const pos = String(i + 1).padStart(9);
|
|
108
|
+
const chunk = sequence.slice(i, i + 60).toLowerCase();
|
|
109
|
+
const groups = chunk.match(/.{1,10}/g) || [];
|
|
110
|
+
lines.push(`${pos} ${groups.join(' ')}`);
|
|
111
|
+
}
|
|
112
|
+
lines.push('//');
|
|
113
|
+
return lines;
|
|
114
|
+
}
|
|
83
115
|
export async function stringifyGBK({ features, assemblyName, session, }) {
|
|
84
116
|
if (!features.length) {
|
|
85
117
|
return '';
|
|
@@ -103,6 +135,5 @@ export async function stringifyGBK({ features, assemblyName, session, }) {
|
|
|
103
135
|
region: { assemblyName, start, end, refName },
|
|
104
136
|
})) ?? '';
|
|
105
137
|
const lines = features.map(feat => formatFeatWithSubfeatures(feat, start));
|
|
106
|
-
|
|
107
|
-
return [l1, l2, ...lines, ...seqlines].join('\n');
|
|
138
|
+
return `${[l1, l2, ...lines, ...formatOrigin(contig)].join('\n')}\n`;
|
|
108
139
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const coreFields = new Set([
|
|
2
2
|
'uniqueId',
|
|
3
|
+
'id',
|
|
3
4
|
'refName',
|
|
4
5
|
'source',
|
|
5
6
|
'type',
|
|
@@ -14,10 +15,8 @@ const coreFields = new Set([
|
|
|
14
15
|
'_lineHash',
|
|
15
16
|
]);
|
|
16
17
|
const retitle = {
|
|
17
|
-
id: 'ID',
|
|
18
18
|
name: 'Name',
|
|
19
19
|
alias: 'Alias',
|
|
20
|
-
parent: 'Parent',
|
|
21
20
|
target: 'Target',
|
|
22
21
|
gap: 'Gap',
|
|
23
22
|
derives_from: 'Derives_from',
|
|
@@ -27,31 +26,51 @@ const retitle = {
|
|
|
27
26
|
ontology_term: 'Ontology_term',
|
|
28
27
|
is_circular: 'Is_circular',
|
|
29
28
|
};
|
|
29
|
+
function encodeGFF3Value(str) {
|
|
30
|
+
return str
|
|
31
|
+
.replace(/%/g, '%25')
|
|
32
|
+
.replace(/;/g, '%3B')
|
|
33
|
+
.replace(/=/g, '%3D')
|
|
34
|
+
.replace(/&/g, '%26')
|
|
35
|
+
.replace(/,/g, '%2C')
|
|
36
|
+
.replace(/\t/g, '%09')
|
|
37
|
+
.replace(/\n/g, '%0A')
|
|
38
|
+
.replace(/\r/g, '%0D');
|
|
39
|
+
}
|
|
30
40
|
function fmt(obj) {
|
|
31
|
-
if (
|
|
32
|
-
return
|
|
41
|
+
if (obj === null || obj === undefined) {
|
|
42
|
+
return undefined;
|
|
33
43
|
}
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
if (Array.isArray(obj)) {
|
|
45
|
+
const items = obj.map(o => fmt(o)).filter(o => o !== undefined);
|
|
46
|
+
return items.length > 0 ? items.join(',') : undefined;
|
|
36
47
|
}
|
|
37
|
-
|
|
38
|
-
return
|
|
48
|
+
if (typeof obj === 'object') {
|
|
49
|
+
return encodeGFF3Value(JSON.stringify(obj));
|
|
39
50
|
}
|
|
51
|
+
return encodeGFF3Value(String(obj));
|
|
40
52
|
}
|
|
41
53
|
function formatAttributes(f, parentId) {
|
|
42
54
|
const attributes = [];
|
|
55
|
+
const id = f.get('id');
|
|
56
|
+
if (id) {
|
|
57
|
+
attributes.push(`ID=${encodeGFF3Value(String(id))}`);
|
|
58
|
+
}
|
|
43
59
|
if (parentId) {
|
|
44
|
-
attributes.push(`Parent=${parentId}`);
|
|
60
|
+
attributes.push(`Parent=${encodeGFF3Value(String(parentId))}`);
|
|
45
61
|
}
|
|
46
62
|
const tags = Object.keys(f.toJSON()).filter(tag => !coreFields.has(tag));
|
|
63
|
+
const hasDescription = tags.includes('description');
|
|
64
|
+
const hasNote = tags.includes('note');
|
|
47
65
|
for (const tag of tags) {
|
|
48
66
|
const val = f.get(tag);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const formattedVal = fmt(val);
|
|
68
|
+
if (formattedVal) {
|
|
69
|
+
let key = retitle[tag] || tag;
|
|
70
|
+
if (tag === 'note' && hasDescription && hasNote) {
|
|
71
|
+
key = 'note2';
|
|
54
72
|
}
|
|
73
|
+
attributes.push(`${key}=${formattedVal}`);
|
|
55
74
|
}
|
|
56
75
|
}
|
|
57
76
|
return attributes.join(';');
|
|
@@ -84,8 +103,8 @@ export function formatMultiLevelFeat(feature, parentId, parentRef) {
|
|
|
84
103
|
].join('\n');
|
|
85
104
|
}
|
|
86
105
|
export function stringifyGFF3({ features }) {
|
|
87
|
-
return [
|
|
106
|
+
return `${[
|
|
88
107
|
'##gff-version 3',
|
|
89
108
|
...features.map(f => formatMultiLevelFeat(f)),
|
|
90
|
-
].join('\n')
|
|
109
|
+
].join('\n')}\n`;
|
|
91
110
|
}
|
|
@@ -21,9 +21,12 @@ export class LayoutSession {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
cachedLayoutIsValid(cachedLayout) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
const bpPerPxMatch = cachedLayout.props.bpPerPx === this.props.bpPerPx;
|
|
25
|
+
const currentConfig = readConfObject(this.props.config);
|
|
26
|
+
const cachedConfig = readConfObject(cachedLayout.props.config);
|
|
27
|
+
const configMatch = deepEqual(currentConfig, cachedConfig);
|
|
28
|
+
const filtersMatch = deepEqual(this.props.filters, cachedLayout.props.filters);
|
|
29
|
+
return bpPerPxMatch && configMatch && filtersMatch;
|
|
27
30
|
}
|
|
28
31
|
get layout() {
|
|
29
32
|
if (!this.cachedLayout || !this.cachedLayoutIsValid(this.cachedLayout)) {
|
|
@@ -73,7 +73,10 @@ export default class ServerSideRenderer extends RendererType {
|
|
|
73
73
|
pluginManager: this.pluginManager,
|
|
74
74
|
}),
|
|
75
75
|
filters: args.filters
|
|
76
|
-
? new SerializableFilterChain({
|
|
76
|
+
? new SerializableFilterChain({
|
|
77
|
+
filters: args.filters,
|
|
78
|
+
jexl: this.pluginManager.jexl,
|
|
79
|
+
})
|
|
77
80
|
: undefined,
|
|
78
81
|
};
|
|
79
82
|
}
|
|
@@ -6,8 +6,9 @@ interface Filter {
|
|
|
6
6
|
export type SerializedFilterChain = string[];
|
|
7
7
|
export default class SerializableFilterChain {
|
|
8
8
|
filterChain: Filter[];
|
|
9
|
-
constructor({ filters }: {
|
|
9
|
+
constructor({ filters, jexl, }: {
|
|
10
10
|
filters: SerializedFilterChain;
|
|
11
|
+
jexl?: unknown;
|
|
11
12
|
});
|
|
12
13
|
passes(...args: any[]): boolean;
|
|
13
14
|
toJSON(): {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { stringToJexlExpression } from "../../../util/jexlStrings.js";
|
|
2
2
|
export default class SerializableFilterChain {
|
|
3
3
|
filterChain;
|
|
4
|
-
constructor({ filters }) {
|
|
4
|
+
constructor({ filters, jexl, }) {
|
|
5
5
|
this.filterChain = filters
|
|
6
6
|
.map(f => f.trim())
|
|
7
7
|
.filter(f => !!f)
|
|
8
8
|
.map(inputFilter => {
|
|
9
9
|
if (typeof inputFilter === 'string') {
|
|
10
|
-
const expr = stringToJexlExpression(inputFilter);
|
|
10
|
+
const expr = stringToJexlExpression(inputFilter, jexl);
|
|
11
11
|
return { expr, string: inputFilter };
|
|
12
12
|
}
|
|
13
13
|
throw new Error(`invalid inputFilter string "${inputFilter}"`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateRendererType } from "./util.js";
|
|
2
|
-
import RpcMethodType from "../../pluggableElementTypes/RpcMethodType.js";
|
|
2
|
+
import RpcMethodType, { convertFileHandleLocations, } from "../../pluggableElementTypes/RpcMethodType.js";
|
|
3
3
|
import { renameRegionsIfNeeded } from "../../util/index.js";
|
|
4
|
+
import { getBlobMap, setBlobMap } from "../../util/tracks.js";
|
|
4
5
|
export default class CoreRender extends RpcMethodType {
|
|
5
6
|
name = 'CoreRender';
|
|
6
7
|
async serializeArguments(args, rpcDriver) {
|
|
@@ -18,7 +19,11 @@ export default class CoreRender extends RpcMethodType {
|
|
|
18
19
|
if (!sessionId) {
|
|
19
20
|
throw new Error('must pass a unique session id');
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
const { renderingProps, ...rest } = renamedArgs;
|
|
23
|
+
const blobMap = getBlobMap();
|
|
24
|
+
convertFileHandleLocations(rest, blobMap);
|
|
25
|
+
setBlobMap(blobMap);
|
|
26
|
+
return validateRendererType(rendererType, this.pluginManager.getRendererType(rendererType)).renderDirect({ ...rest, renderingProps });
|
|
22
27
|
}
|
|
23
28
|
async execute(args, rpcDriver) {
|
|
24
29
|
const deserializedArgs = await this.deserializeArguments(args, rpcDriver);
|
package/esm/ui/CascadingMenu.js
CHANGED
|
@@ -5,9 +5,16 @@ import { Divider, ListItemIcon, ListItemText, ListSubheader, Menu, MenuItem, } f
|
|
|
5
5
|
import CascadingMenuHelpIconButton from "./CascadingMenuHelpIconButton.js";
|
|
6
6
|
import HoverMenu from "./HoverMenu.js";
|
|
7
7
|
import { MenuItemEndDecoration } from "./MenuItems.js";
|
|
8
|
+
function labelToTestId(label) {
|
|
9
|
+
if (typeof label === 'string') {
|
|
10
|
+
return label.toLowerCase().replace(/\s+/g, '_');
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
8
14
|
function CascadingSubmenu({ title, Icon, inset, menuItems, onMenuItemClick, closeAfterItemClick, onCloseRoot, isOpen, onOpen, onClose, }) {
|
|
9
15
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
10
|
-
|
|
16
|
+
const testId = labelToTestId(title);
|
|
17
|
+
return (_jsxs(_Fragment, { children: [_jsxs(MenuItem, { ref: setAnchorEl, "data-testid": testId ? `cascading-submenu-${testId}` : undefined, onMouseOver: onOpen, onFocus: onOpen, onClick: onOpen, children: [Icon ? (_jsx(ListItemIcon, { children: _jsx(Icon, {}) })) : null, _jsx(ListItemText, { primary: title, inset: inset }), _jsx(ChevronRight, {})] }), _jsx(HoverMenu, { open: isOpen, anchorEl: anchorEl, onClose: onClose, anchorOrigin: { vertical: 'top', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'left' }, children: _jsx(CascadingMenuList, { closeAfterItemClick: closeAfterItemClick, onMenuItemClick: onMenuItemClick, menuItems: menuItems, onCloseRoot: onCloseRoot }) })] }));
|
|
11
18
|
}
|
|
12
19
|
function CascadingMenuList({ onMenuItemClick, closeAfterItemClick, menuItems, onCloseRoot, }) {
|
|
13
20
|
const [openSubmenuIdx, setOpenSubmenuIdx] = useState();
|
|
@@ -34,7 +41,8 @@ function CascadingMenuList({ onMenuItemClick, closeAfterItemClick, menuItems, on
|
|
|
34
41
|
const actionItem = item;
|
|
35
42
|
const helpText = actionItem.helpText;
|
|
36
43
|
const isCheckOrRadio = actionItem.type === 'checkbox' || actionItem.type === 'radio';
|
|
37
|
-
|
|
44
|
+
const itemTestId = labelToTestId(actionItem.label);
|
|
45
|
+
return (_jsxs(MenuItem, { "data-testid": itemTestId ? `cascading-menuitem-${itemTestId}` : undefined, disabled: Boolean(actionItem.disabled), onClick: event => {
|
|
38
46
|
if (closeAfterItemClick) {
|
|
39
47
|
onCloseRoot();
|
|
40
48
|
}
|
|
@@ -6,6 +6,5 @@ declare const FileSelector: ({ inline, location, name, description, rootModel, s
|
|
|
6
6
|
inline?: boolean;
|
|
7
7
|
rootModel?: AbstractRootModel;
|
|
8
8
|
setLocation: (param: FileLocation) => void;
|
|
9
|
-
setName?: (str: string) => void;
|
|
10
9
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
11
10
|
export default FileSelector;
|
|
@@ -5,48 +5,36 @@ import { observer } from 'mobx-react';
|
|
|
5
5
|
import LocationInput from "./LocationInput.js";
|
|
6
6
|
import SourceTypeSelector from "./SourceTypeSelector.js";
|
|
7
7
|
import useInternetAccounts from "./useInternetAccounts.js";
|
|
8
|
+
import { addAccountToLocation, getInitialSourceType } from "./util.js";
|
|
8
9
|
import { notEmpty } from "../../util/index.js";
|
|
9
10
|
import { isUriLocation } from "../../util/types/index.js";
|
|
10
|
-
function getInitialToggleValue(location) {
|
|
11
|
-
if (location &&
|
|
12
|
-
'internetAccountId' in location &&
|
|
13
|
-
location.internetAccountId) {
|
|
14
|
-
return location.internetAccountId;
|
|
15
|
-
}
|
|
16
|
-
return !location || isUriLocation(location) ? 'url' : 'file';
|
|
17
|
-
}
|
|
18
11
|
const FileSelector = observer(function FileSelector({ inline, location, name, description, rootModel, setLocation, }) {
|
|
19
|
-
const [
|
|
12
|
+
const [sourceType, setSourceType] = useState(() => getInitialSourceType(location));
|
|
20
13
|
const { accountMap, shownAccountIds, hiddenAccountIds, recentlyUsed, setRecentlyUsed, } = useInternetAccounts(rootModel);
|
|
21
|
-
const selectedAccount = accountMap[
|
|
22
|
-
const
|
|
23
|
-
setLocation(
|
|
24
|
-
...loc,
|
|
25
|
-
...(selectedAccount && isUriLocation(loc)
|
|
26
|
-
? { internetAccountId: selectedAccount.internetAccountId }
|
|
27
|
-
: {}),
|
|
28
|
-
});
|
|
14
|
+
const selectedAccount = accountMap[sourceType];
|
|
15
|
+
const handleLocationChange = useCallback((loc) => {
|
|
16
|
+
setLocation(addAccountToLocation(loc, selectedAccount));
|
|
29
17
|
}, [setLocation, selectedAccount]);
|
|
30
18
|
useEffect(() => {
|
|
31
19
|
if (selectedAccount &&
|
|
32
20
|
isUriLocation(location) &&
|
|
33
21
|
location.internetAccountId !== selectedAccount.internetAccountId) {
|
|
34
|
-
|
|
22
|
+
handleLocationChange(location);
|
|
35
23
|
}
|
|
36
|
-
}, [location, selectedAccount,
|
|
37
|
-
const
|
|
38
|
-
setRecentlyUsed([
|
|
39
|
-
...new Set([newValue, ...recentlyUsed].filter(notEmpty)),
|
|
40
|
-
]);
|
|
24
|
+
}, [location, selectedAccount, handleLocationChange]);
|
|
25
|
+
const handleSourceTypeChange = useCallback((newValue) => {
|
|
41
26
|
if (newValue) {
|
|
42
|
-
|
|
27
|
+
setRecentlyUsed([
|
|
28
|
+
...new Set([newValue, ...recentlyUsed].filter(notEmpty)),
|
|
29
|
+
]);
|
|
30
|
+
setSourceType(newValue);
|
|
31
|
+
if (isUriLocation(location)) {
|
|
32
|
+
handleLocationChange(location);
|
|
33
|
+
}
|
|
43
34
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return (_jsxs(_Fragment, { children: [_jsx(Box, { display: "flex", children: _jsx(InputLabel, { shrink: true, children: name }) }), _jsx(FormGroup, { children: _jsxs(Box, { display: "flex", flexDirection: inline ? 'row' : 'column', gap: 0.5, children: [_jsx(SourceTypeSelector, { value: toggleButtonValue, shownAccountIds: shownAccountIds, hiddenAccountIds: hiddenAccountIds, accountMap: accountMap, onChange: (_event, newValue) => {
|
|
49
|
-
selectSourceType(newValue);
|
|
50
|
-
}, onHiddenAccountSelect: selectSourceType }), _jsx(LocationInput, { toggleButtonValue: toggleButtonValue, selectedAccount: selectedAccount, location: location, inline: inline, setLocation: setLocationWithAccount })] }) }), _jsx(FormHelperText, { children: description })] }));
|
|
35
|
+
}, [location, recentlyUsed, setRecentlyUsed, handleLocationChange]);
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsx(Box, { display: "flex", children: _jsx(InputLabel, { shrink: true, children: name }) }), _jsx(FormGroup, { children: _jsxs(Box, { display: "flex", flexDirection: inline ? 'row' : 'column', gap: 0.5, children: [_jsx(SourceTypeSelector, { value: sourceType, shownAccountIds: shownAccountIds, hiddenAccountIds: hiddenAccountIds, accountMap: accountMap, onChange: (_event, newValue) => {
|
|
37
|
+
handleSourceTypeChange(newValue);
|
|
38
|
+
}, onHiddenAccountSelect: handleSourceTypeChange }), _jsx(LocationInput, { toggleButtonValue: sourceType, selectedAccount: selectedAccount, location: location, inline: inline, setLocation: handleLocationChange })] }) }), _jsx(FormHelperText, { children: description })] }));
|
|
51
39
|
});
|
|
52
40
|
export default FileSelector;
|