@datagrok/bio 2.26.5 → 2.26.7
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/CHANGELOG.md +10 -0
- package/CLAUDE.md +0 -14
- package/css/monomer-collections.css +12 -2
- package/dist/422.js +1 -1
- package/dist/422.js.map +1 -1
- package/dist/package-test.js +3 -3
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +2 -2
- package/dist/package.js.map +1 -1
- package/package.json +2 -2
- package/src/package-api.ts +6 -2
- package/src/package.g.ts +13 -2
- package/src/package.ts +22 -3
- package/src/tests/projects-tests.ts +0 -4
- package/src/utils/monomer-cell-renderer.ts +14 -8
- package/src/utils/monomer-lib/library-file-manager/ui.ts +3 -2
- package/src/utils/monomer-lib/monomer-collection-handler.ts +359 -0
- package/src/utils/monomer-lib/monomer-collections-view.ts +167 -9
- package/src/widgets/monomer-info-widget.ts +79 -0
- package/test-console-output-1.log +599 -511
- package/test-record-1.mp4 +0 -0
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "Davit Rizhinashvili",
|
|
6
6
|
"email": "drizhinashvili@datagrok.ai"
|
|
7
7
|
},
|
|
8
|
-
"version": "2.26.
|
|
8
|
+
"version": "2.26.7",
|
|
9
9
|
"description": "Bioinformatics support (import/export of sequences, conversion, visualization, analysis). [See more](https://github.com/datagrok-ai/public/blob/master/packages/Bio/README.md) for details.",
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
],
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@biowasm/aioli": "^3.1.0",
|
|
47
|
-
"@datagrok-libraries/bio": "^5.63.
|
|
47
|
+
"@datagrok-libraries/bio": "^5.63.6",
|
|
48
48
|
"@datagrok-libraries/chem-meta": "^1.2.9",
|
|
49
49
|
"@datagrok-libraries/math": "^1.2.6",
|
|
50
50
|
"@datagrok-libraries/ml": "^6.10.11",
|
package/src/package-api.ts
CHANGED
|
@@ -121,6 +121,10 @@ export namespace funcs {
|
|
|
121
121
|
return await grok.functions.call('Bio:CompositionAnalysisWidget', { sequence });
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export async function monomerInfoPanel(monomerSv: any ): Promise<any> {
|
|
125
|
+
return await grok.functions.call('Bio:MonomerInfoPanel', { monomerSv });
|
|
126
|
+
}
|
|
127
|
+
|
|
124
128
|
export async function macromoleculeDifferenceCellRenderer(): Promise<any> {
|
|
125
129
|
return await grok.functions.call('Bio:MacromoleculeDifferenceCellRenderer', {});
|
|
126
130
|
}
|
|
@@ -380,8 +384,8 @@ export namespace funcs {
|
|
|
380
384
|
return await grok.functions.call('Bio:ManageMonomersView', {});
|
|
381
385
|
}
|
|
382
386
|
|
|
383
|
-
export async function manageMonomerLibrariesView(): Promise<DG.View> {
|
|
384
|
-
return await grok.functions.call('Bio:ManageMonomerLibrariesView', {});
|
|
387
|
+
export async function manageMonomerLibrariesView(path?: string ): Promise<DG.View> {
|
|
388
|
+
return await grok.functions.call('Bio:ManageMonomerLibrariesView', { path });
|
|
385
389
|
}
|
|
386
390
|
|
|
387
391
|
export async function manageMonomerLibrariesViewTreeBrowser(treeNode: any ): Promise<void> {
|
package/src/package.g.ts
CHANGED
|
@@ -160,6 +160,16 @@ export function compositionAnalysisWidget(sequence: DG.SemanticValue) : any {
|
|
|
160
160
|
return PackageFunctions.compositionAnalysisWidget(sequence);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
//name: Monomer
|
|
164
|
+
//tags: bio, panel
|
|
165
|
+
//input: semantic_value monomerSv { semType: Monomer }
|
|
166
|
+
//output: widget result
|
|
167
|
+
//meta.domain: bio
|
|
168
|
+
//meta.role: panel
|
|
169
|
+
export function monomerInfoPanel(monomerSv: DG.SemanticValue) : any {
|
|
170
|
+
return PackageFunctions.monomerInfoPanel(monomerSv);
|
|
171
|
+
}
|
|
172
|
+
|
|
163
173
|
//name: MacromoleculeDifferenceCellRenderer
|
|
164
174
|
//tags: cellRenderer
|
|
165
175
|
//output: grid_cell_renderer result
|
|
@@ -599,12 +609,13 @@ export async function manageMonomersView() : Promise<void> {
|
|
|
599
609
|
|
|
600
610
|
//name: Manage Monomer Libraries
|
|
601
611
|
//tags: app
|
|
612
|
+
//input: string path { meta.url: true; optional: true }
|
|
602
613
|
//output: view result
|
|
603
614
|
//meta.role: app
|
|
604
615
|
//meta.browsePath: Peptides
|
|
605
616
|
//meta.icon: files/icons/monomers.png
|
|
606
|
-
export async function manageMonomerLibrariesView() : Promise<any> {
|
|
607
|
-
return await PackageFunctions.manageMonomerLibrariesView();
|
|
617
|
+
export async function manageMonomerLibrariesView(path?: string) : Promise<any> {
|
|
618
|
+
return await PackageFunctions.manageMonomerLibrariesView(path);
|
|
608
619
|
}
|
|
609
620
|
|
|
610
621
|
//name: Monomer Manager Tree Browser
|
package/src/package.ts
CHANGED
|
@@ -44,10 +44,12 @@ import {SequenceDiversityViewer} from './analysis/sequence-diversity-viewer';
|
|
|
44
44
|
import {invalidateMols, MONOMERIC_COL_TAGS, SubstructureSearchDialog} from './substructure-search/substructure-search';
|
|
45
45
|
import {convert} from './utils/convert';
|
|
46
46
|
import {getMacromoleculeColumnPropertyPanel} from './widgets/representations';
|
|
47
|
+
import {getMonomerInfoWidget} from './widgets/monomer-info-widget';
|
|
47
48
|
import {saveAsFastaUI} from './utils/save-as-fasta';
|
|
48
49
|
import {BioSubstructureFilter} from './widgets/bio-substructure-filter';
|
|
49
50
|
import {WebLogoViewer} from './viewers/web-logo-viewer';
|
|
50
51
|
import {MonomerLibManager} from './utils/monomer-lib/lib-manager';
|
|
52
|
+
import {MonomerCollectionHandler} from './utils/monomer-lib/monomer-collection-handler';
|
|
51
53
|
import {getMonomerLibraryManagerLink, showManageLibrariesDialog, showManageLibrariesView} from './utils/monomer-lib/library-file-manager/ui';
|
|
52
54
|
import {demoBioSimDiv} from './demo/bio01-similarity-diversity';
|
|
53
55
|
import {demoSeqSpace} from './demo/bio01a-hierarchical-clustering-and-sequence-space';
|
|
@@ -382,6 +384,15 @@ export class PackageFunctions {
|
|
|
382
384
|
return getCompositionAnalysisWidget(sequence, _package.monomerLib, _package.seqHelper);
|
|
383
385
|
}
|
|
384
386
|
|
|
387
|
+
@grok.decorators.panel({name: 'Monomer',
|
|
388
|
+
tags: ['bio', 'panel'],
|
|
389
|
+
meta: {domain: 'bio'},
|
|
390
|
+
})
|
|
391
|
+
static monomerInfoPanel(
|
|
392
|
+
@grok.decorators.param({options: {semType: 'Monomer'}}) monomerSv: DG.SemanticValue): DG.Widget {
|
|
393
|
+
return getMonomerInfoWidget(monomerSv.value as string, _package.monomerLib);
|
|
394
|
+
}
|
|
395
|
+
|
|
385
396
|
@grok.decorators.func({
|
|
386
397
|
name: 'MacromoleculeDifferenceCellRenderer',
|
|
387
398
|
tags: ['cellRenderer'],
|
|
@@ -1309,8 +1320,12 @@ export class PackageFunctions {
|
|
|
1309
1320
|
browsePath: 'Peptides',
|
|
1310
1321
|
icon: 'files/icons/monomers.png',
|
|
1311
1322
|
})
|
|
1312
|
-
static async manageMonomerLibrariesView(
|
|
1313
|
-
|
|
1323
|
+
static async manageMonomerLibrariesView(
|
|
1324
|
+
@grok.decorators.param({options: {metaUrl: true, optional: true}}) path?: string
|
|
1325
|
+
): Promise<DG.View> {
|
|
1326
|
+
const res = await showManageLibrariesView(false);
|
|
1327
|
+
res.parentCall = grok.functions.getCurrentCall();
|
|
1328
|
+
return res;
|
|
1314
1329
|
}
|
|
1315
1330
|
|
|
1316
1331
|
// @grok.decorators.func({tags: ['monomer-lib-provider'], result: {type: 'object', name: 'result'}})
|
|
@@ -1327,7 +1342,8 @@ export class PackageFunctions {
|
|
|
1327
1342
|
// eslint-disable-next-line rxjs/no-ignored-subscription, rxjs/no-async-subscribe
|
|
1328
1343
|
libNode.onSelected.subscribe(async () => {
|
|
1329
1344
|
const monomerManager = await MonomerManager.getInstance();
|
|
1330
|
-
await monomerManager.getViewRoot(libName, true);
|
|
1345
|
+
const res = await monomerManager.getViewRoot(libName, true);
|
|
1346
|
+
res.parentCall = grok.functions.getCurrentCall();
|
|
1331
1347
|
monomerManager.resetCurrentRowFollowing();
|
|
1332
1348
|
});
|
|
1333
1349
|
});
|
|
@@ -1667,6 +1683,9 @@ async function initBioInt() {
|
|
|
1667
1683
|
|
|
1668
1684
|
// hydrophobPalette = new SeqPaletteCustom(palette);
|
|
1669
1685
|
|
|
1686
|
+
// Register object handlers
|
|
1687
|
+
DG.ObjectHandler.register(new MonomerCollectionHandler());
|
|
1688
|
+
|
|
1670
1689
|
_package.logger.debug(`${logPrefix}, end`);
|
|
1671
1690
|
handleSequenceHeaderRendering();
|
|
1672
1691
|
}
|
|
@@ -14,11 +14,8 @@ import {undefinedColor} from '@datagrok-libraries/bio/src/utils/cell-renderer-mo
|
|
|
14
14
|
import {HelmType, HelmTypes, PolymerType, PolymerTypes} from '@datagrok-libraries/js-draw-lite/src/types/org';
|
|
15
15
|
import {polymerTypeToHelmType} from '@datagrok-libraries/bio/src/utils/macromolecule/utils';
|
|
16
16
|
|
|
17
|
-
const Tags = new class {
|
|
18
|
-
tooltipHandlerTemp = 'tooltip-handler.Monomer';
|
|
19
|
-
}();
|
|
20
|
-
|
|
21
17
|
const DASH_GAP_SYMBOL = '-';
|
|
18
|
+
const MONOMER_MARGIN = 2;
|
|
22
19
|
|
|
23
20
|
export class MonomerCellRendererBack extends CellRendererWithMonomerLibBackBase {
|
|
24
21
|
constructor(gridCol: DG.GridColumn | null, tableCol: DG.Column) {
|
|
@@ -68,6 +65,8 @@ export class MonomerCellRendererBack extends CellRendererWithMonomerLibBackBase
|
|
|
68
65
|
g.font = `12px monospace`;
|
|
69
66
|
g.textBaseline = 'middle';
|
|
70
67
|
g.textAlign = 'left';
|
|
68
|
+
const mMet = g.measureText('M');
|
|
69
|
+
const lineHeight = mMet.actualBoundingBoxAscent + mMet.actualBoundingBoxDescent + MONOMER_MARGIN * 4;
|
|
71
70
|
|
|
72
71
|
let value: string = gridCell.cell.value;
|
|
73
72
|
// render original value
|
|
@@ -80,10 +79,11 @@ export class MonomerCellRendererBack extends CellRendererWithMonomerLibBackBase
|
|
|
80
79
|
//cell width of monomer should dictate how many characters can be displayed
|
|
81
80
|
// for width 40, 6 characters can be displayed (0.15 is 6 / 40)
|
|
82
81
|
const shortSymbols = symbols.map((s) => monomerToShort(s, Math.max(2, Math.floor(w * 0.15 / symbols.length))));
|
|
83
|
-
const symbolWidths = shortSymbols.map((s) => g.measureText(s).width);
|
|
82
|
+
const symbolWidths = shortSymbols.map((s) => g.measureText(s).width + MONOMER_MARGIN * 2);
|
|
83
|
+
// symbolWidths[symbolWidths.length - 1] -= MONOMER_MARGIN;
|
|
84
84
|
const totalWidth = symbolWidths.reduce((a, b) => a + b, 0);
|
|
85
85
|
const xOffset = (w - totalWidth) / 2;
|
|
86
|
-
let xPos = x + xOffset;
|
|
86
|
+
let xPos = x + xOffset + MONOMER_MARGIN;
|
|
87
87
|
const alphabet = this.tableCol.getTag(bioTAGS.alphabet);
|
|
88
88
|
const biotype = alphabet === ALPHABET.RNA || alphabet === ALPHABET.DNA ? HelmTypes.NUCLEOTIDE : HelmTypes.AA;
|
|
89
89
|
for (let i = 0; i < shortSymbols.length; i++) {
|
|
@@ -100,9 +100,15 @@ export class MonomerCellRendererBack extends CellRendererWithMonomerLibBackBase
|
|
|
100
100
|
} else
|
|
101
101
|
textcolor = this.monomerLib.getMonomerTextColor(actBioType, symbol);
|
|
102
102
|
}
|
|
103
|
-
if (applyToBackground
|
|
103
|
+
if (applyToBackground) {
|
|
104
104
|
g.fillStyle = backgroundcolor;
|
|
105
|
-
|
|
105
|
+
if (w < 30 || h < 30)
|
|
106
|
+
g.fillRect(x + i * w / shortSymbols.length, y, w / shortSymbols.length, h);
|
|
107
|
+
else {
|
|
108
|
+
const radius = MONOMER_MARGIN;
|
|
109
|
+
g.roundRect(xPos - MONOMER_MARGIN, y + (h / 2) - lineHeight / 2, symbolWidths[i], lineHeight, radius);
|
|
110
|
+
g.fill();
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
g.fillStyle = textcolor;
|
|
108
114
|
g.fillText(shortSymbols[i], xPos, y + (h / 2), w);
|
|
@@ -83,7 +83,8 @@ class MonomerLibraryManagerWidget {
|
|
|
83
83
|
|
|
84
84
|
private async getWidgetContent(): Promise<HTMLElement> {
|
|
85
85
|
const libControlsForm = await LibraryControlsManager.createControlsForm();
|
|
86
|
-
$(libControlsForm).addClass('monomer-lib-controls-form')
|
|
86
|
+
$(libControlsForm).addClass('monomer-lib-controls-form')
|
|
87
|
+
.addClass('d4-dialog-contents');
|
|
87
88
|
setTimeout(() => {
|
|
88
89
|
libControlsForm && $(libControlsForm) && $(libControlsForm).removeClass('ui-form-condensed');
|
|
89
90
|
}, 200);
|
|
@@ -174,7 +175,7 @@ class LibraryControlsManager {
|
|
|
174
175
|
private async _createControlsForm(): Promise<HTMLElement> {
|
|
175
176
|
const libraryControls = await this.createLibraryControls();
|
|
176
177
|
const inputsForm = ui.wideForm(libraryControls, undefined);
|
|
177
|
-
$(inputsForm).addClass('monomer-lib-controls-form');
|
|
178
|
+
$(inputsForm).addClass('monomer-lib-controls-form').addClass('d4-dialog-contents');
|
|
178
179
|
|
|
179
180
|
return inputsForm;
|
|
180
181
|
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
import * as grok from 'datagrok-api/grok';
|
|
3
|
+
import * as ui from 'datagrok-api/ui';
|
|
4
|
+
import * as DG from 'datagrok-api/dg';
|
|
5
|
+
|
|
6
|
+
import {MonomerCollection, IMonomerLib} from '@datagrok-libraries/bio/src/types/monomer-library';
|
|
7
|
+
import {HelmType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
|
|
8
|
+
import {HelmTypes} from '@datagrok-libraries/bio/src/helm/consts';
|
|
9
|
+
import {MonomerSelectionWidget} from '@datagrok-libraries/bio/src/utils/monomer-selection-dialog';
|
|
10
|
+
|
|
11
|
+
import {MonomerLibManager} from './lib-manager';
|
|
12
|
+
import {SEM_TYPES} from '../constants';
|
|
13
|
+
import {_package} from '../../package';
|
|
14
|
+
|
|
15
|
+
/** Wrapper that pairs a collection name with its loaded data. */
|
|
16
|
+
export interface MonomerCollectionInfo {
|
|
17
|
+
name: string; // file name (with .json)
|
|
18
|
+
displayName: string; // name without .json
|
|
19
|
+
data: MonomerCollection;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const HANDLER_TYPE = 'MonomerCollection';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* ObjectHandler for one or more monomer collections.
|
|
26
|
+
* When a single collection is selected it renders details in the context panel.
|
|
27
|
+
* When multiple collections are selected it renders merge / bulk-delete actions.
|
|
28
|
+
*/
|
|
29
|
+
export class MonomerCollectionHandler extends DG.ObjectHandler<MonomerCollectionInfo | MonomerCollectionInfo[]> {
|
|
30
|
+
get type() { return HANDLER_TYPE; }
|
|
31
|
+
get name() { return 'Monomer Collection'; }
|
|
32
|
+
|
|
33
|
+
isApplicable(x: any): boolean {
|
|
34
|
+
if (Array.isArray(x))
|
|
35
|
+
return x.length > 0 && x.every((item: any) => this.isSingleCollection(item));
|
|
36
|
+
return this.isSingleCollection(x);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private isSingleCollection(x: any): boolean {
|
|
40
|
+
return x != null && typeof x === 'object' && 'name' in x && 'displayName' in x && 'data' in x &&
|
|
41
|
+
typeof (x as MonomerCollectionInfo).data?.monomerSymbols !== 'undefined';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getCaption(x: MonomerCollectionInfo | MonomerCollectionInfo[]): string {
|
|
45
|
+
if (Array.isArray(x))
|
|
46
|
+
return `${x.length} collections`;
|
|
47
|
+
return x.displayName;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
renderProperties(x: MonomerCollectionInfo | MonomerCollectionInfo[], context: any = null): HTMLElement {
|
|
51
|
+
if (Array.isArray(x))
|
|
52
|
+
return this.renderMultiProperties(x);
|
|
53
|
+
return this.renderSingleProperties(x);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -- Single collection panel --
|
|
57
|
+
|
|
58
|
+
private renderSingleProperties(info: MonomerCollectionInfo): HTMLElement {
|
|
59
|
+
const acc = ui.accordion('Monomer Collection');
|
|
60
|
+
const currentUser = DG.User.current().login;
|
|
61
|
+
const isOwner = info.data.updatedBy === currentUser;
|
|
62
|
+
|
|
63
|
+
// Details pane
|
|
64
|
+
acc.addPane('Details', () => {
|
|
65
|
+
const map: {[key: string]: any} = {
|
|
66
|
+
'Name': info.displayName,
|
|
67
|
+
};
|
|
68
|
+
if (info.data.description)
|
|
69
|
+
map['Description'] = info.data.description;
|
|
70
|
+
if (info.data.tags && info.data.tags.length > 0)
|
|
71
|
+
map['Tags'] = info.data.tags.join(', ');
|
|
72
|
+
map['Monomers'] = `${info.data.monomerSymbols.length}`;
|
|
73
|
+
if (info.data.updatedBy)
|
|
74
|
+
map['Updated by'] = info.data.updatedBy;
|
|
75
|
+
if (info.data.updatedOn) {
|
|
76
|
+
try { map['Updated on'] = new Date(info.data.updatedOn).toLocaleString(); } catch { /* ignore */ }
|
|
77
|
+
}
|
|
78
|
+
return ui.tableFromMap(map);
|
|
79
|
+
}, true);
|
|
80
|
+
|
|
81
|
+
// Monomers pane
|
|
82
|
+
acc.addPane('Monomers', () => {
|
|
83
|
+
const container = ui.div([], {style: {display: 'flex', flexWrap: 'wrap', gap: '4px'}});
|
|
84
|
+
const monomerLib = this.getMonomerLib();
|
|
85
|
+
for (const symbol of info.data.monomerSymbols) {
|
|
86
|
+
const tag = ui.div([symbol], {
|
|
87
|
+
style: {
|
|
88
|
+
display: 'inline-block', padding: '2px 8px', borderRadius: '4px',
|
|
89
|
+
background: 'var(--grey-1)', border: '1px solid var(--grey-2)',
|
|
90
|
+
fontSize: '11px', color: 'var(--grey-5)', cursor: 'pointer',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
tag.addEventListener('mouseenter', () => {
|
|
94
|
+
if (monomerLib) {
|
|
95
|
+
const tooltipEl = MonomerCollectionHandler.getMonomerTooltipSafe(monomerLib, symbol);
|
|
96
|
+
if (tooltipEl) {
|
|
97
|
+
const rect = tag.getBoundingClientRect();
|
|
98
|
+
ui.tooltip.show(tooltipEl, rect.left, rect.bottom + 4);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
tag.addEventListener('mouseleave', () => ui.tooltip.hide());
|
|
103
|
+
tag.addEventListener('click', () => {
|
|
104
|
+
grok.shell.o = DG.SemanticValue.fromValueType(symbol, SEM_TYPES.MONOMER);
|
|
105
|
+
});
|
|
106
|
+
container.appendChild(tag);
|
|
107
|
+
}
|
|
108
|
+
return container;
|
|
109
|
+
}, true);
|
|
110
|
+
|
|
111
|
+
// Actions pane
|
|
112
|
+
acc.addPane('Actions', () => {
|
|
113
|
+
const actions: HTMLElement[] = [];
|
|
114
|
+
|
|
115
|
+
const editBtn = ui.button('Edit', async () => {
|
|
116
|
+
if (isOwner) await this.editCollection(info);
|
|
117
|
+
}, isOwner ? 'Edit this collection' : 'Only the author can edit');
|
|
118
|
+
if (!isOwner) MonomerCollectionHandler.disableButton(editBtn);
|
|
119
|
+
actions.push(editBtn);
|
|
120
|
+
|
|
121
|
+
const deleteBtn = ui.button('Delete', async () => {
|
|
122
|
+
if (isOwner) await this.deleteCollection(info);
|
|
123
|
+
}, isOwner ? 'Delete this collection' : 'Only the author can delete');
|
|
124
|
+
if (!isOwner) MonomerCollectionHandler.disableButton(deleteBtn);
|
|
125
|
+
actions.push(deleteBtn);
|
|
126
|
+
|
|
127
|
+
actions.push(ui.button('Duplicate', async () => {
|
|
128
|
+
await this.duplicateCollection(info);
|
|
129
|
+
}, 'Create a copy of this collection'));
|
|
130
|
+
|
|
131
|
+
return ui.divH(actions);
|
|
132
|
+
}, true);
|
|
133
|
+
|
|
134
|
+
return acc.root;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// -- Multi-collection panel --
|
|
138
|
+
|
|
139
|
+
private renderMultiProperties(items: MonomerCollectionInfo[]): HTMLElement {
|
|
140
|
+
const acc = ui.accordion('Monomer Collections');
|
|
141
|
+
const currentUser = DG.User.current().login;
|
|
142
|
+
const allOwned = items.every((c) => c.data.updatedBy === currentUser);
|
|
143
|
+
|
|
144
|
+
// Summary pane
|
|
145
|
+
acc.addPane('Selection', () => {
|
|
146
|
+
const list = ui.list(items.map((c) => c.displayName));
|
|
147
|
+
return ui.divV([
|
|
148
|
+
ui.divText(`${items.length} collections selected`),
|
|
149
|
+
list,
|
|
150
|
+
]);
|
|
151
|
+
}, true);
|
|
152
|
+
|
|
153
|
+
// Actions pane
|
|
154
|
+
acc.addPane('Actions', () => {
|
|
155
|
+
const actions: HTMLElement[] = [];
|
|
156
|
+
|
|
157
|
+
actions.push(ui.button('Merge', async () => {
|
|
158
|
+
await this.mergeCollections(items);
|
|
159
|
+
}, 'Merge selected collections into one'));
|
|
160
|
+
|
|
161
|
+
const deleteAllBtn = ui.button('Delete All', async () => {
|
|
162
|
+
if (allOwned) await this.deleteMultipleCollections(items);
|
|
163
|
+
}, allOwned ? 'Delete all selected collections' : 'Only the author can delete (you must be author of all)');
|
|
164
|
+
if (!allOwned) MonomerCollectionHandler.disableButton(deleteAllBtn);
|
|
165
|
+
actions.push(deleteAllBtn);
|
|
166
|
+
|
|
167
|
+
return ui.divH(actions);
|
|
168
|
+
}, true);
|
|
169
|
+
|
|
170
|
+
return acc.root;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// -- Actions --
|
|
174
|
+
|
|
175
|
+
private async editCollection(info: MonomerCollectionInfo): Promise<void> {
|
|
176
|
+
// Find existing view and trigger edit
|
|
177
|
+
const view = Array.from(grok.shell.views).find((v) => v.name === 'Monomer Collections');
|
|
178
|
+
if (view && (view as any)._mcView) {
|
|
179
|
+
const mcView = (view as any)._mcView as import('./monomer-collections-view').MonomerCollectionsView;
|
|
180
|
+
mcView.editCollectionPublic(info.name, info.data);
|
|
181
|
+
} else {
|
|
182
|
+
// Fallback: open the edit dialog directly
|
|
183
|
+
const libHelper = await MonomerLibManager.getInstance();
|
|
184
|
+
await libHelper.awaitLoaded();
|
|
185
|
+
const monomerLib = libHelper.getMonomerLib();
|
|
186
|
+
this.showEditDialog(info, libHelper, monomerLib!);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private showEditDialog(info: MonomerCollectionInfo, libHelper: MonomerLibManager, monomerLib: IMonomerLib): void {
|
|
191
|
+
const polymerTypes: PolymerType[] = ['PEPTIDE', 'RNA', 'CHEM'];
|
|
192
|
+
const nameInput = ui.input.string('Name', {value: info.displayName, nullable: false});
|
|
193
|
+
const descInput = ui.input.string('Description', {value: info.data.description ?? '', nullable: true});
|
|
194
|
+
const tagsInput = ui.input.string('Tags', {value: (info.data.tags ?? []).join(', '), nullable: true, placeholder: 'Comma-separated tags'});
|
|
195
|
+
const polymerTypeInput = ui.input.choice('Polymer Type', {items: [...polymerTypes], value: 'PEPTIDE', nullable: false});
|
|
196
|
+
const monomerWidget = new MonomerSelectionWidget(monomerLib, polymerTypeInput.value as PolymerType, [...(info.data.monomerSymbols ?? [])], libHelper);
|
|
197
|
+
|
|
198
|
+
// eslint-disable-next-line rxjs/no-ignored-subscription
|
|
199
|
+
polymerTypeInput.onChanged.subscribe(() => monomerWidget.setPolymerType(polymerTypeInput.value as PolymerType));
|
|
200
|
+
|
|
201
|
+
const dlg = ui.dialog({title: `Edit Collection: ${info.displayName}`})
|
|
202
|
+
.add(ui.divV([nameInput, descInput, tagsInput, polymerTypeInput, ui.element('hr'), monomerWidget.root]))
|
|
203
|
+
.onOK(async () => {
|
|
204
|
+
const name = nameInput.value?.trim();
|
|
205
|
+
if (!name) { grok.shell.warning('Collection name is required'); return; }
|
|
206
|
+
const selectedMonomers = monomerWidget.getSelectedMonomers();
|
|
207
|
+
if (selectedMonomers.length === 0) { grok.shell.warning('Please select at least one monomer'); return; }
|
|
208
|
+
const tags = this.parseTags(tagsInput.value);
|
|
209
|
+
try {
|
|
210
|
+
if (name !== info.displayName)
|
|
211
|
+
await libHelper.deleteMonomerCollection(info.name);
|
|
212
|
+
await libHelper.addOrUpdateMonomerCollection(name, selectedMonomers, descInput.value ?? undefined, tags.length > 0 ? tags : undefined);
|
|
213
|
+
grok.shell.info(`Collection "${name}" saved successfully`);
|
|
214
|
+
await this.refreshView();
|
|
215
|
+
} catch (err: any) {
|
|
216
|
+
grok.shell.error('Error saving collection');
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
.show({resizable: true});
|
|
220
|
+
dlg.root.style.width = '550px';
|
|
221
|
+
monomerWidget.focus();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async deleteCollection(info: MonomerCollectionInfo): Promise<void> {
|
|
225
|
+
ui.dialog({title: 'Delete Collection'})
|
|
226
|
+
.add(ui.divText(`Are you sure you want to delete "${info.displayName}"?`))
|
|
227
|
+
.onOK(async () => {
|
|
228
|
+
try {
|
|
229
|
+
const libHelper = await MonomerLibManager.getInstance();
|
|
230
|
+
await libHelper.deleteMonomerCollection(info.name);
|
|
231
|
+
grok.shell.info(`Collection "${info.displayName}" deleted`);
|
|
232
|
+
await this.refreshView();
|
|
233
|
+
} catch (err: any) {
|
|
234
|
+
grok.shell.error('Error deleting collection');
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
.show();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private async duplicateCollection(info: MonomerCollectionInfo): Promise<void> {
|
|
241
|
+
const nameInput = ui.input.string('New Name', {value: `${info.displayName} (copy)`, nullable: false});
|
|
242
|
+
ui.dialog({title: 'Duplicate Collection'})
|
|
243
|
+
.add(nameInput)
|
|
244
|
+
.onOK(async () => {
|
|
245
|
+
const name = nameInput.value?.trim();
|
|
246
|
+
if (!name) { grok.shell.warning('Name is required'); return; }
|
|
247
|
+
try {
|
|
248
|
+
const libHelper = await MonomerLibManager.getInstance();
|
|
249
|
+
await libHelper.addOrUpdateMonomerCollection(name, [...info.data.monomerSymbols], info.data.description, info.data.tags ? [...info.data.tags] : undefined);
|
|
250
|
+
grok.shell.info(`Collection "${name}" created`);
|
|
251
|
+
await this.refreshView();
|
|
252
|
+
} catch (err: any) {
|
|
253
|
+
grok.shell.error('Error duplicating collection');
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
.show();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async mergeCollections(items: MonomerCollectionInfo[]): Promise<void> {
|
|
260
|
+
const nameInput = ui.input.string('Merged Name', {value: 'Merged Collection', nullable: false});
|
|
261
|
+
const descInput = ui.input.string('Description', {nullable: true, placeholder: 'Optional description'});
|
|
262
|
+
ui.dialog({title: `Merge ${items.length} Collections`})
|
|
263
|
+
.add(ui.divV([
|
|
264
|
+
ui.divText(`Merging: ${items.map((c) => c.displayName).join(', ')}`),
|
|
265
|
+
nameInput,
|
|
266
|
+
descInput,
|
|
267
|
+
]))
|
|
268
|
+
.onOK(async () => {
|
|
269
|
+
const name = nameInput.value?.trim();
|
|
270
|
+
if (!name) { grok.shell.warning('Name is required'); return; }
|
|
271
|
+
try {
|
|
272
|
+
const allSymbols = [...new Set(items.flatMap((c) => c.data.monomerSymbols))];
|
|
273
|
+
const allTags = [...new Set(items.flatMap((c) => c.data.tags ?? []))];
|
|
274
|
+
const libHelper = await MonomerLibManager.getInstance();
|
|
275
|
+
await libHelper.addOrUpdateMonomerCollection(name, allSymbols, descInput.value ?? undefined, allTags.length > 0 ? allTags : undefined);
|
|
276
|
+
grok.shell.info(`Merged collection "${name}" created with ${allSymbols.length} monomers`);
|
|
277
|
+
await this.refreshView();
|
|
278
|
+
} catch (err: any) {
|
|
279
|
+
grok.shell.error('Error merging collections');
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
.show();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private async deleteMultipleCollections(items: MonomerCollectionInfo[]): Promise<void> {
|
|
286
|
+
ui.dialog({title: 'Delete Collections'})
|
|
287
|
+
.add(ui.divText(`Are you sure you want to delete ${items.length} collections?\n${items.map((c) => c.displayName).join(', ')}`))
|
|
288
|
+
.onOK(async () => {
|
|
289
|
+
try {
|
|
290
|
+
const libHelper = await MonomerLibManager.getInstance();
|
|
291
|
+
for (const item of items)
|
|
292
|
+
await libHelper.deleteMonomerCollection(item.name);
|
|
293
|
+
grok.shell.info(`${items.length} collections deleted`);
|
|
294
|
+
await this.refreshView();
|
|
295
|
+
} catch (err: any) {
|
|
296
|
+
grok.shell.error('Error deleting collections');
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
.show();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async refreshView(): Promise<void> {
|
|
303
|
+
const view = Array.from(grok.shell.views).find((v) => v.name === 'Monomer Collections');
|
|
304
|
+
if (view && (view as any)._mcView) {
|
|
305
|
+
const mcView = (view as any)._mcView as import('./monomer-collections-view').MonomerCollectionsView;
|
|
306
|
+
await mcView.refresh();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private parseTags(input: string | null): string[] {
|
|
311
|
+
if (!input) return [];
|
|
312
|
+
return input.split(',').map((t) => t.trim()).filter((t) => t.length > 0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Gets the current monomer lib (synchronous, returns null if not loaded). */
|
|
316
|
+
private getMonomerLib(): IMonomerLib | null {
|
|
317
|
+
return _package.monomerLib ?? null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Gets a monomer tooltip element, searching across all HELM types. */
|
|
321
|
+
private static getMonomerTooltipSafe(monomerLib: IMonomerLib, symbol: string): HTMLElement | null {
|
|
322
|
+
const helmTypes: HelmType[] = [HelmTypes.AA, HelmTypes.NUCLEOTIDE, HelmTypes.CHEM, HelmTypes.BLOB];
|
|
323
|
+
for (const ht of helmTypes) {
|
|
324
|
+
try {
|
|
325
|
+
const wem = monomerLib.getWebEditorMonomer(ht, symbol);
|
|
326
|
+
if (wem) return monomerLib.getTooltip(ht, symbol);
|
|
327
|
+
} catch { /* skip */ }
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Applies custom disabled styling to a button (greyed out, not clickable). */
|
|
333
|
+
private static disableButton(btn: HTMLElement): void {
|
|
334
|
+
btn.style.opacity = '0.4';
|
|
335
|
+
btn.style.cursor = 'default';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Builds a context menu for a single collection. */
|
|
339
|
+
static buildContextMenu(menu: DG.Menu, info: MonomerCollectionInfo, handler: MonomerCollectionHandler): void {
|
|
340
|
+
const currentUser = DG.User.current().login;
|
|
341
|
+
const isOwner = info.data.updatedBy === currentUser;
|
|
342
|
+
|
|
343
|
+
menu.item('Edit', () => { if (isOwner) handler.editCollection(info); }, null,
|
|
344
|
+
{isEnabled: () => isOwner ? null : 'Only the author can edit'});
|
|
345
|
+
menu.item('Delete', () => { if (isOwner) handler.deleteCollection(info); }, null,
|
|
346
|
+
{isEnabled: () => isOwner ? null : 'Only the author can delete'});
|
|
347
|
+
menu.item('Duplicate', () => handler.duplicateCollection(info));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Builds a context menu for multiple collections. */
|
|
351
|
+
static buildMultiContextMenu(menu: DG.Menu, items: MonomerCollectionInfo[], handler: MonomerCollectionHandler): void {
|
|
352
|
+
const currentUser = DG.User.current().login;
|
|
353
|
+
const allOwned = items.every((c) => c.data.updatedBy === currentUser);
|
|
354
|
+
|
|
355
|
+
menu.item('Merge', () => handler.mergeCollections(items));
|
|
356
|
+
menu.item('Delete All', () => { if (allOwned) handler.deleteMultipleCollections(items); }, null,
|
|
357
|
+
{isEnabled: () => allOwned ? null : 'Only the author can delete (you must be author of all)'});
|
|
358
|
+
}
|
|
359
|
+
}
|