@datagrok/sequence-translator 1.10.5 → 1.10.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/detectors.js +3 -1
  3. package/dist/455.js +1 -1
  4. package/dist/455.js.map +1 -1
  5. package/dist/package-test.js +1 -1
  6. package/dist/package-test.js.map +1 -1
  7. package/dist/package.js +1 -1
  8. package/dist/package.js.map +1 -1
  9. package/package.json +9 -6
  10. package/scripts/build-monomer-lib.py +0 -1
  11. package/src/apps/common/view/components/colored-input/input-painters.ts +1 -1
  12. package/src/apps/structure/model/monomer-code-parser.ts +1 -1
  13. package/src/apps/translator/model/conversion-utils.ts +1 -1
  14. package/src/demo/demo-st-ui.ts +1 -1
  15. package/src/package-test.ts +1 -1
  16. package/src/package.g.ts +14 -0
  17. package/src/package.ts +27 -13
  18. package/src/polytool/pt-enumerate-seq-dialog.ts +54 -7
  19. package/src/polytool/pt-monomer-selection-dialog.ts +200 -0
  20. package/src/polytool/pt-placeholders-breadth-input.ts +39 -0
  21. package/src/polytool/pt-placeholders-input.ts +34 -0
  22. package/src/tests/files-tests.ts +1 -1
  23. package/src/tests/formats-support.ts +1 -1
  24. package/src/tests/formats-to-helm.ts +1 -1
  25. package/src/tests/helm-to-nucleotides.ts +1 -1
  26. package/src/tests/polytool-chain-from-notation-tests.ts +1 -1
  27. package/src/tests/polytool-chain-parse-notation-tests.ts +1 -1
  28. package/src/tests/polytool-convert-tests.ts +1 -1
  29. package/src/tests/polytool-detectors-custom-notation-test.ts +1 -1
  30. package/src/tests/polytool-enumerate-breadth-tests.ts +1 -1
  31. package/src/tests/polytool-enumerate-tests.ts +1 -1
  32. package/src/tests/polytool-unrule-tests.ts +1 -1
  33. package/src/tests/toAtomicLevel-tests.ts +1 -1
  34. package/src/tests/utils/detect-macromolecule-utils.ts +1 -1
  35. package/src/utils/cyclized.ts +8 -2
  36. package/test-console-output-1.log +207 -1594
  37. package/test-record-1.mp4 +0 -0
  38. package/webpack.config.js +22 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/sequence-translator",
3
3
  "friendlyName": "Sequence Translator",
4
- "version": "1.10.5",
4
+ "version": "1.10.7",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
@@ -22,13 +22,13 @@
22
22
  }
23
23
  ],
24
24
  "dependencies": {
25
- "@datagrok-libraries/bio": "^5.60.2",
25
+ "@datagrok-libraries/bio": "^5.61.6",
26
26
  "@datagrok-libraries/chem-meta": "^1.2.8",
27
27
  "@datagrok-libraries/tutorials": "^1.6.1",
28
28
  "@datagrok-libraries/utils": "^4.6.5",
29
29
  "@types/react": "^18.0.15",
30
30
  "cash-dom": "^8.1.0",
31
- "datagrok-api": "^1.25.0",
31
+ "datagrok-api": "^1.26.0",
32
32
  "lodash": "^4.17.21",
33
33
  "object-hash": "^3.0.0",
34
34
  "openchemlib": "6.0.1",
@@ -36,7 +36,8 @@
36
36
  "ts-loader": "^9.3.1",
37
37
  "typeahead-standalone": "4.14.1",
38
38
  "typescript": "^5.4.2",
39
- "wu": "^2.1.0"
39
+ "wu": "^2.1.0",
40
+ "@datagrok-libraries/test": "^1.1.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@datagrok-libraries/helm-web-editor": "^1.1.16",
@@ -54,7 +55,6 @@
54
55
  "@typescript-eslint/eslint-plugin": "^7.2.0",
55
56
  "@typescript-eslint/parser": "^7.2.0",
56
57
  "css-loader": "^6.7.3",
57
- "datagrok-tools": "^4.14.55",
58
58
  "eslint": "^8.57.0",
59
59
  "eslint-config-google": "^0.14.0",
60
60
  "style-loader": "^3.3.1",
@@ -89,5 +89,8 @@
89
89
  "canView": [
90
90
  "All users"
91
91
  ],
92
- "category": "Bioinformatics"
92
+ "category": "Bioinformatics",
93
+ "overrides": {
94
+ "datagrok-api": "$datagrok-api"
95
+ }
93
96
  }
@@ -1,4 +1,3 @@
1
- # pylint: disable=no-member
2
1
  import os.path
3
2
  import sys
4
3
  from io import TextIOWrapper
@@ -24,7 +24,7 @@ export function demoPainter(input: string): HTMLSpanElement[] {
24
24
  return spans;
25
25
  }
26
26
 
27
- // todo: port to another place
27
+ /* todo: port to another place */
28
28
  export function highlightInvalidSubsequence(input: string, th: ITranslationHelper): HTMLSpanElement[] {
29
29
  // validate sequence
30
30
  let cutoff = 0;
@@ -84,7 +84,7 @@ export class MonomerSequenceParser {
84
84
  }
85
85
  }
86
86
 
87
- // todo: to be eliminated after full helm support
87
+ /* todo: to be eliminated after full helm support */
88
88
  function monomerHasLeftPhosphateLinker(monomerSymbol: string): boolean {
89
89
  return _package.jsonData.monomersWithPhosphate['left'].includes(monomerSymbol);
90
90
  }
@@ -51,7 +51,7 @@ export function getNucleotidesSequence(helmString: string, monomerLib: MonomerLi
51
51
  return nucleotides;
52
52
  }
53
53
 
54
- // todo: remove after refactoring as a workaround
54
+ /* todo: remove after refactoring as a workaround */
55
55
  export function convert(
56
56
  sequence: string, sourceFormat: string, targetFormat: string, th: ITranslationHelper
57
57
  ): string | null {
@@ -2,7 +2,7 @@ import * as grok from 'datagrok-api/grok';
2
2
  import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
- import {delay} from '@datagrok-libraries/utils/src/test';
5
+ import {delay} from '@datagrok-libraries/test/src/test';
6
6
  import {PackageFunctions} from '../package';
7
7
  import {tryCatch} from '../apps/common/model/helpers';
8
8
 
@@ -2,7 +2,7 @@ import * as grok from 'datagrok-api/grok';
2
2
  import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
- import {runTests, tests, TestContext, initAutoTests as initTests} from '@datagrok-libraries/utils/src/test';
5
+ import {runTests, tests, TestContext, initAutoTests as initTests} from '@datagrok-libraries/test/src/test';
6
6
 
7
7
  import './tests/formats-to-helm';
8
8
  import './tests/helm-to-nucleotides';
package/src/package.g.ts CHANGED
@@ -4,6 +4,7 @@ import * as DG from 'datagrok-api/dg';
4
4
  //name: Oligo Toolkit
5
5
  //tags: app
6
6
  //output: view result
7
+ //meta.role: app
7
8
  //meta.icon: img/icons/toolkit.png
8
9
  //meta.browsePath: Peptides | Oligo Toolkit
9
10
  export async function oligoToolkitApp() : Promise<any> {
@@ -11,6 +12,7 @@ export async function oligoToolkitApp() : Promise<any> {
11
12
  }
12
13
 
13
14
  //tags: init
15
+ //meta.role: init
14
16
  export async function init() : Promise<void> {
15
17
  await PackageFunctions.init();
16
18
  }
@@ -18,6 +20,7 @@ export async function init() : Promise<void> {
18
20
  //name: Oligo Translator
19
21
  //tags: app
20
22
  //output: view result
23
+ //meta.role: app
21
24
  //meta.icon: img/icons/translator.png
22
25
  //meta.browsePath: Peptides | Oligo Toolkit
23
26
  export async function oligoTranslatorApp() : Promise<any> {
@@ -27,6 +30,7 @@ export async function oligoTranslatorApp() : Promise<any> {
27
30
  //name: Oligo Pattern
28
31
  //tags: app
29
32
  //output: view result
33
+ //meta.role: app
30
34
  //meta.icon: img/icons/pattern.png
31
35
  //meta.browsePath: Peptides | Oligo Toolkit
32
36
  export async function oligoPatternApp() : Promise<any> {
@@ -36,6 +40,7 @@ export async function oligoPatternApp() : Promise<any> {
36
40
  //name: Oligo Structure
37
41
  //tags: app
38
42
  //output: view result
43
+ //meta.role: app
39
44
  //meta.icon: img/icons/structure.png
40
45
  //meta.browsePath: Peptides | Oligo Toolkit
41
46
  export async function oligoStructureApp() : Promise<any> {
@@ -113,6 +118,7 @@ export async function polyToolConvertTopMenu() : Promise<void> {
113
118
  //tags: editor
114
119
  //input: funccall call
115
120
  //output: column result
121
+ //meta.role: editor
116
122
  export async function getPolyToolConvertEditor(call: DG.FuncCall) : Promise<any> {
117
123
  return await PackageFunctions.getPolyToolConvertEditor(call);
118
124
  }
@@ -157,6 +163,7 @@ export async function createMonomerLibraryForPolyTool(file: DG.FileInfo) : Promi
157
163
  //tags: app
158
164
  //meta.icon: img/icons/structure.png
159
165
  //meta.browsePath: Peptides | PolyTool
166
+ //meta.role: app
160
167
  export async function ptEnumeratorHelmApp() : Promise<void> {
161
168
  await PackageFunctions.ptEnumeratorHelmApp();
162
169
  }
@@ -165,6 +172,7 @@ export async function ptEnumeratorHelmApp() : Promise<void> {
165
172
  //tags: app
166
173
  //meta.icon: img/icons/structure.png
167
174
  //meta.browsePath: Peptides | PolyTool
175
+ //meta.role: app
168
176
  export async function ptEnumeratorChemApp() : Promise<void> {
169
177
  await PackageFunctions.ptEnumeratorChemApp();
170
178
  }
@@ -213,3 +221,9 @@ export async function getPolyToolCombineDialog() : Promise<void> {
213
221
  export function applyNotationProviderForCyclized(col: DG.Column<any>, separator: string) : void {
214
222
  PackageFunctions.applyNotationProviderForCyclized(col, separator);
215
223
  }
224
+
225
+ //output: dynamic result
226
+ //meta.role: notationProviderConstructor
227
+ export async function harmonizedSequenceNotationProviderConstructor() : Promise<any> {
228
+ return await PackageFunctions.harmonizedSequenceNotationProviderConstructor();
229
+ }
package/src/package.ts CHANGED
@@ -3,7 +3,7 @@ import * as grok from 'datagrok-api/grok';
3
3
  import * as ui from 'datagrok-api/ui';
4
4
  import * as DG from 'datagrok-api/dg';
5
5
 
6
- import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
6
+ import {NOTATION, NOTATION_PROVIDER_CONSTRUCTOR_ROLE} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
7
7
  import {SeqTemps} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
8
8
 
9
9
  import {OligoToolkitPackage} from './apps/common/model/oligo-toolkit-package';
@@ -79,7 +79,8 @@ export class PackageFunctions {
79
79
  @grok.decorators.app({
80
80
  icon: 'img/icons/toolkit.png',
81
81
  browsePath: 'Peptides | Oligo Toolkit',
82
- name: 'Oligo Toolkit'
82
+ name: 'Oligo Toolkit',
83
+ tags: ['app']
83
84
  })
84
85
  static async oligoToolkitApp(): Promise<DG.ViewBase> {
85
86
  await _package.initLibData();
@@ -92,7 +93,7 @@ export class PackageFunctions {
92
93
  }
93
94
 
94
95
 
95
- @grok.decorators.init()
96
+ @grok.decorators.init({tags: ['init']})
96
97
  static async init(): Promise<void> {
97
98
  if (initSequenceTranslatorPromise === null)
98
99
  _package.startInit(initSequenceTranslatorPromise = initSequenceTranslatorInt());
@@ -103,7 +104,8 @@ export class PackageFunctions {
103
104
  @grok.decorators.app({
104
105
  icon: 'img/icons/translator.png',
105
106
  browsePath: 'Peptides | Oligo Toolkit',
106
- name: 'Oligo Translator'
107
+ name: 'Oligo Translator',
108
+ tags: ['app']
107
109
  })
108
110
  static async oligoTranslatorApp(): Promise<DG.ViewBase> {
109
111
  const view = await getSpecifiedAppView(APP_NAME.TRANSLATOR);
@@ -114,7 +116,8 @@ export class PackageFunctions {
114
116
  @grok.decorators.app({
115
117
  icon: 'img/icons/pattern.png',
116
118
  browsePath: 'Peptides | Oligo Toolkit',
117
- name: 'Oligo Pattern'
119
+ name: 'Oligo Pattern',
120
+ tags: ['app']
118
121
  })
119
122
  static async oligoPatternApp(): Promise<DG.ViewBase> {
120
123
  const view = await getSpecifiedAppView(APP_NAME.PATTERN);
@@ -125,7 +128,8 @@ export class PackageFunctions {
125
128
  @grok.decorators.app({
126
129
  icon: 'img/icons/structure.png',
127
130
  browsePath: 'Peptides | Oligo Toolkit',
128
- name: 'Oligo Structure'
131
+ name: 'Oligo Structure',
132
+ tags: ['app']
129
133
  })
130
134
  static async oligoStructureApp(): Promise<DG.ViewBase> {
131
135
  const view = await getSpecifiedAppView(APP_NAME.STRUCTURE);
@@ -229,7 +233,7 @@ export class PackageFunctions {
229
233
  await polyToolConvertUI();
230
234
  }
231
235
 
232
- @grok.decorators.editor()
236
+ @grok.decorators.editor({tags: ['editor']})
233
237
  static async getPolyToolConvertEditor(
234
238
  call: DG.FuncCall): Promise<DG.Column<string> | null> {
235
239
  const funcEditor = await PolyToolConvertFuncEditor.create(call);
@@ -296,10 +300,11 @@ export class PackageFunctions {
296
300
  @grok.decorators.func({
297
301
  meta: {
298
302
  icon: 'img/icons/structure.png',
299
- browsePath: 'Peptides | PolyTool'
303
+ browsePath: 'Peptides | PolyTool',
304
+ role: 'app'
300
305
  },
301
- tags: ['app'],
302
- name: 'HELM Enumerator'
306
+ name: 'HELM Enumerator',
307
+ tags: ['app']
303
308
  })
304
309
  static async ptEnumeratorHelmApp(): Promise<void> {
305
310
  await polyToolEnumerateHelmUI();
@@ -309,10 +314,11 @@ export class PackageFunctions {
309
314
  @grok.decorators.func({
310
315
  meta: {
311
316
  icon: 'img/icons/structure.png',
312
- browsePath: 'Peptides | PolyTool'
317
+ browsePath: 'Peptides | PolyTool',
318
+ role: 'app'
313
319
  },
314
- tags: ['app'],
315
- name: 'Chem Enumerator'
320
+ name: 'Chem Enumerator',
321
+ tags: ['app']
316
322
  })
317
323
  static async ptEnumeratorChemApp(): Promise<void> {
318
324
  polyToolEnumerateChemUI();
@@ -394,6 +400,14 @@ export class PackageFunctions {
394
400
  col.tags[PolyToolTags.dataRole] = 'template';
395
401
  col.temp[SeqTemps.notationProvider] = new CyclizedNotationProvider(separator, _package.helmHelper);
396
402
  }
403
+
404
+ @grok.decorators.func({
405
+ name: 'harmonizedSequenceNotationProviderConstructor',
406
+ meta: {role: 'notationProviderConstructor'}
407
+ })
408
+ static async harmonizedSequenceNotationProviderConstructor(): Promise<typeof CyclizedNotationProvider> {
409
+ return CyclizedNotationProvider;
410
+ }
397
411
  }
398
412
 
399
413
  //name: getSpecifiedAppView
@@ -19,11 +19,13 @@ import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
19
19
  import {InputColumnBase} from '@datagrok-libraries/bio/src/types/input';
20
20
  import {SeqValueBase} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
21
21
  import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
22
+ import {SeqTemps} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
22
23
 
23
24
  import {PolyToolEnumeratorParams, PolyToolEnumeratorType, PolyToolEnumeratorTypes} from './types';
24
25
  import {getLibrariesList, LIB_PATH} from './utils';
25
26
  import {doPolyToolEnumerateHelm, PT_HELM_EXAMPLE} from './pt-enumeration-helm';
26
27
  import {PolyToolPlaceholdersInput} from './pt-placeholders-input';
28
+ import {showMonomerSelectionDialog} from './pt-monomer-selection-dialog';
27
29
  import {defaultErrorHandler} from '../utils/err-info';
28
30
  import {PolyToolPlaceholdersBreadthInput} from './pt-placeholders-breadth-input';
29
31
  import {PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
@@ -32,11 +34,13 @@ import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './conversion/pt-rules'
32
34
  import {Chain} from './conversion/pt-chain';
33
35
  import {polyToolConvert} from './pt-dialog';
34
36
 
35
- import {_package, PackageFunctions} from '../package';
37
+ import {_package, applyNotationProviderForCyclized, PackageFunctions} from '../package';
36
38
  import {buildMonomerHoverLink} from '@datagrok-libraries/bio/src/monomer-works/monomer-hover';
37
39
  import {getRdKitModule} from '@datagrok-libraries/bio/src/chem/rdkit-module';
38
40
 
39
41
  import {PolymerTypes} from '@datagrok-libraries/js-draw-lite/src/types/org';
42
+ import { CyclizedNotationProvider } from '../utils/cyclized';
43
+ import { INotationProvider } from '@datagrok-libraries/bio/src/utils/macromolecule/types';
40
44
 
41
45
  type PolyToolEnumerateInputs = {
42
46
  macromolecule: HelmInputBase;
@@ -161,9 +165,22 @@ async function getPolyToolEnumerateDialog(
161
165
  const getValue = (cell?: DG.Cell): [SeqValueBase, PolyToolDataRole] => {
162
166
  let resSeqValue: SeqValueBase;
163
167
  let resDataRole: PolyToolDataRole;
168
+ let resHelm: string | null = null;
164
169
  if (cell && cell.rowIndex >= 0 && cell?.column.semType == DG.SEMTYPE.MACROMOLECULE) {
165
170
  const sh = seqHelper.getSeqHandler(cell.column);
166
171
  resSeqValue = sh.getValue(cell.rowIndex);
172
+ // if (cell.column.temp?.[SeqTemps.notationProvider])
173
+ if (cell.column.temp?.[SeqTemps.notationProvider] && !(cell.column.temp[SeqTemps.notationProvider] instanceof CyclizedNotationProvider)) {
174
+ const notationProvider = cell.column.temp[SeqTemps.notationProvider] as INotationProvider;
175
+ resHelm = notationProvider.getHelm(resSeqValue.value, {});
176
+ // create temp helm column to apply notation provider for cyclized sequences
177
+ let seqCol: DG.Column;
178
+ DG.DataFrame.fromColumns([seqCol = DG.Column.fromList(DG.COLUMN_TYPE.STRING, 'seq', [resHelm])]);
179
+ seqCol.semType = DG.SEMTYPE.MACROMOLECULE;
180
+ seqCol.meta.units = NOTATION.HELM;
181
+ const sh = seqHelper.getSeqHandler(seqCol);
182
+ resSeqValue = sh.getValue(0);
183
+ }
167
184
  resDataRole = (resSeqValue.tags[PolyToolTags.dataRole] as PolyToolDataRole.template) ?? PolyToolDataRole.macromolecule;
168
185
  } else {
169
186
  const seqCol = DG.Column.fromList(DG.COLUMN_TYPE.STRING, 'seq', [PT_HELM_EXAMPLE]);
@@ -200,8 +217,13 @@ async function getPolyToolEnumerateDialog(
200
217
  if (aa.T === 'ATOM') {
201
218
  try {
202
219
  if (!seqValue.isDna() && !seqValue.isRna()) {
203
- const canonicalSymbol = seqValue.getSplitted().getCanonical(aa.bio!.continuousId - 1);
204
- return monomerLibFuncs.getMonomer(aa.bio!.type, canonicalSymbol);
220
+ if (cell?.column?.temp?.[SeqTemps.notationProvider] instanceof CyclizedNotationProvider) {
221
+ const canonicalSymbol = seqValue.getSplitted().getCanonical(aa.bio!.continuousId - 1);
222
+ return monomerLibFuncs.getMonomer(aa.bio!.type, canonicalSymbol);
223
+ } else {
224
+ const canonicalSymbol = aa.elem;
225
+ return monomerLibFuncs.getMonomer(aa.bio!.type, canonicalSymbol);
226
+ }
205
227
  } else {
206
228
  const canonicalSymbol = seqValue.getSplittedWithSugarsAndPhosphates().getCanonical(aa.bio!.continuousId - 1);
207
229
  return monomerLibFuncs.getMonomer(aa.bio!.type, canonicalSymbol);
@@ -273,6 +295,30 @@ async function getPolyToolEnumerateDialog(
273
295
  inputs.library.root.style.setProperty('display', 'none');
274
296
  inputs.trivialNameCol.addOptions(trivialNameSampleDiv);
275
297
 
298
+ // Wire up monomer cell double-click to open selection dialog
299
+ inputs.placeholders.onMonomerCellEdit = async (position: number, currentMonomers: string[]) => {
300
+ const mol = inputs.macromolecule.molValue;
301
+ if (position < 0 || position >= mol.atoms.length)
302
+ return null;
303
+ const atom = mol.atoms[position];
304
+ const helmType: HelmType = atom.biotype()!;
305
+ const polymerType = helmTypeToPolymerType(helmType);
306
+ return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
307
+ };
308
+
309
+ // Wire up breadth monomer cell double-click to open selection dialog
310
+ inputs.placeholdersBreadth.onMonomerCellEdit = async (
311
+ start: number, _end: number, currentMonomers: string[],
312
+ ) => {
313
+ const mol = inputs.macromolecule.molValue;
314
+ if (start < 0 || start >= mol.atoms.length)
315
+ return null;
316
+ const atom = mol.atoms[start];
317
+ const helmType: HelmType = atom.biotype()!;
318
+ const polymerType = helmTypeToPolymerType(helmType);
319
+ return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
320
+ };
321
+
276
322
  let placeholdersValidity: string | null = null;
277
323
  inputs.placeholders.addValidator((value: string): string | null => {
278
324
  const errors: string[] = [];
@@ -475,7 +521,7 @@ async function getPolyToolEnumerateDialog(
475
521
  const cell = grok.shell.tv.dataFrame.currentCell;
476
522
  if (cell.column.semType !== DG.SEMTYPE.MACROMOLECULE) return;
477
523
 
478
- [seqValue, dataRole] = getValue();
524
+ [seqValue, dataRole] = getValue(cell);
479
525
  fillForCurrentCell(seqValue, dataRole, cell);
480
526
  }));
481
527
 
@@ -554,9 +600,9 @@ async function getPolyToolEnumerateDialog(
554
600
  const exec = async (): Promise<void> => {
555
601
  try {
556
602
  const srcHelm = inputs.macromolecule.stringValue;
557
- const helmSelections: number[] = wu.enumerate<HelmAtom>(inputs.macromolecule.molValue.atoms)
603
+ const helmSelections = wu.enumerate<HelmAtom>(inputs.macromolecule.molValue.atoms)
558
604
  .filter(([a, aI]) => a.highlighted)
559
- .map(([a, aI]) => aI).toArray();
605
+ .map(([a, aI]) => a).toArray();
560
606
  if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
561
607
  if (helmSelections.length === 0) {
562
608
  grok.shell.warning('PolyTool: position for enumeration was not selected');
@@ -595,7 +641,8 @@ async function getPolyToolEnumerateDialog(
595
641
  grok.shell.warning(`Monomer Library '${monLibName}' was not found`);
596
642
  return;
597
643
  }
598
- const peptideMonomers = monLib.getMonomerSymbolsByType(PolymerTypes.PEPTIDE);
644
+ const polymerType = helmTypeToPolymerType(helmSelections[0].biotype() ?? 'HELM_AA');
645
+ const peptideMonomers = monLib.getMonomerSymbolsByType(polymerType);
599
646
  placeHoldersValue[0].monomers = peptideMonomers;
600
647
  enumerationType = PolyToolEnumeratorTypes.Single;
601
648
  }
@@ -0,0 +1,200 @@
1
+ /* eslint-disable max-len */
2
+ import * as ui from 'datagrok-api/ui';
3
+ import * as DG from 'datagrok-api/dg';
4
+
5
+ import {HelmType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
6
+ import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types/monomer-library';
7
+ import {polymerTypeToHelmType} from '@datagrok-libraries/bio/src/utils/macromolecule/utils';
8
+
9
+ const MAX_SUGGESTIONS = 20;
10
+
11
+ /** Shows a dialog for selecting monomers with autocomplete and tag-based display.
12
+ * @returns comma-separated monomer symbols, or null if cancelled */
13
+ export async function showMonomerSelectionDialog(
14
+ monomerLib: IMonomerLib, polymerType: PolymerType, presetMonomers?: string[],
15
+ ): Promise<string[] | null> {
16
+ return new Promise<string[] | null>((resolve) => {
17
+ const helmType: HelmType = polymerTypeToHelmType(polymerType);
18
+ const allSymbols = monomerLib.getMonomerSymbolsByType(polymerType);
19
+
20
+ const selectedMonomers: string[] = presetMonomers ? [...presetMonomers] : [];
21
+
22
+ const tagsHost = ui.div([], {style: {display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '8px', maxWidth: '400px'}});
23
+ const input = ui.input.string('Monomers', {value: ''});
24
+ const inputEl = input.input as HTMLInputElement;
25
+ inputEl.setAttribute('autocomplete', 'off');
26
+ inputEl.placeholder = 'Type to search...';
27
+
28
+ let currentMenu: DG.Menu | null = null;
29
+ let menuItems: HTMLElement[] = [];
30
+ let highlightedIndex = -1;
31
+
32
+ function renderTags(): void {
33
+ tagsHost.innerHTML = '';
34
+ for (const symbol of selectedMonomers) {
35
+ const removeBtn = ui.iconFA('times', () => {
36
+ const idx = selectedMonomers.indexOf(symbol);
37
+ if (idx >= 0) {
38
+ selectedMonomers.splice(idx, 1);
39
+ renderTags();
40
+ }
41
+ });
42
+ removeBtn.style.marginLeft = '4px';
43
+ removeBtn.style.cursor = 'pointer';
44
+
45
+ const tag = ui.div([ui.span([symbol]), removeBtn], {
46
+ style: {
47
+ display: 'inline-flex', alignItems: 'center',
48
+ padding: '2px 6px', borderRadius: '4px',
49
+ backgroundColor: 'var(--grey-2)', border: '1px solid var(--grey-3)',
50
+ fontSize: '12px', cursor: 'default',
51
+ },
52
+ });
53
+ // Tooltip on hover
54
+ tag.addEventListener('mouseenter', (e) => {
55
+ const tooltip = monomerLib.getTooltip(helmType, symbol);
56
+ ui.tooltip.show(tooltip, tag.getBoundingClientRect().left, tag.getBoundingClientRect().bottom + 16);
57
+ });
58
+ tag.addEventListener('mouseleave', () => { ui.tooltip.hide(); });
59
+
60
+ tagsHost.appendChild(tag);
61
+ }
62
+ }
63
+
64
+ function addMonomer(symbol: string): void {
65
+ if (!selectedMonomers.includes(symbol)) {
66
+ selectedMonomers.push(symbol);
67
+ renderTags();
68
+ }
69
+ inputEl.value = '';
70
+ hideMenu();
71
+ inputEl.focus();
72
+ }
73
+
74
+ function hideMenu(): void {
75
+ if (currentMenu) {
76
+ currentMenu.hide();
77
+ currentMenu = null;
78
+ }
79
+ menuItems = [];
80
+ highlightedIndex = -1;
81
+ }
82
+
83
+ function getSuggestions(query: string): {symbol: string, monomer: Monomer | null}[] {
84
+ const q = query.toLowerCase();
85
+ const results: {symbol: string, monomer: Monomer | null, rank: number}[] = [];
86
+
87
+ for (const symbol of allSymbols) {
88
+ if (selectedMonomers.includes(symbol))
89
+ continue;
90
+ const monomer = monomerLib.getMonomer(polymerType, symbol);
91
+ const symLower = symbol.toLowerCase();
92
+ const nameLower = monomer?.name?.toLowerCase() ?? '';
93
+
94
+ if (symLower.startsWith(q))
95
+ results.push({symbol, monomer, rank: 0});
96
+ else if (symLower.includes(q))
97
+ results.push({symbol, monomer, rank: 1});
98
+ else if (nameLower.includes(q))
99
+ results.push({symbol, monomer, rank: 2});
100
+ }
101
+
102
+ results.sort((a, b) => a.rank - b.rank || a.symbol.localeCompare(b.symbol));
103
+ return results.slice(0, MAX_SUGGESTIONS);
104
+ }
105
+
106
+ function showSuggestions(): void {
107
+ hideMenu();
108
+ const query = inputEl.value.trim();
109
+ if (!query)
110
+ return;
111
+
112
+ const suggestions = getSuggestions(query);
113
+ if (suggestions.length === 0)
114
+ return;
115
+
116
+ currentMenu = DG.Menu.popup();
117
+ const maxElement = suggestions.reduce((max, s) => {
118
+ const label = s.monomer?.name ? `${s.symbol} - ${s.monomer.name}` : s.symbol;
119
+ if (max.length < label.length)
120
+ return label;
121
+ return max;
122
+ }, '');
123
+ currentMenu.item(maxElement, () => {}); // Dummy item to set menu width
124
+
125
+ const causedBy = new MouseEvent('mousemove', {clientX: inputEl.getBoundingClientRect().left, clientY: inputEl.getBoundingClientRect().bottom});
126
+ currentMenu.show({causedBy: causedBy,
127
+ y: inputEl.offsetHeight + inputEl.offsetTop, x: inputEl.offsetLeft});
128
+
129
+ // collect menu items for keyboard navigation
130
+ setTimeout(() => {
131
+ currentMenu?.clear();
132
+ for (const s of suggestions) {
133
+ const label = s.monomer?.name ? `${s.symbol} - ${s.monomer.name}` : s.symbol;
134
+ currentMenu?.item(label, () => { addMonomer(s.symbol); });
135
+ }
136
+
137
+ const menuRoot = document.querySelector('.d4-menu-popup:last-of-type');
138
+ if (menuRoot)
139
+ menuItems = Array.from(menuRoot.querySelectorAll('.d4-menu-item')) as HTMLElement[];
140
+
141
+ highlightedIndex = -1;
142
+ }, 0);
143
+ }
144
+
145
+ function updateHighlight(newIndex: number): void {
146
+ if (menuItems.length === 0)
147
+ return;
148
+ if (highlightedIndex >= 0 && highlightedIndex < menuItems.length)
149
+ menuItems[highlightedIndex].classList.remove('d4-menu-item-hover');
150
+ highlightedIndex = newIndex;
151
+ if (highlightedIndex >= 0 && highlightedIndex < menuItems.length) {
152
+ menuItems[highlightedIndex].classList.add('d4-menu-item-hover');
153
+ menuItems[highlightedIndex].scrollIntoView({block: 'nearest'});
154
+ }
155
+ }
156
+
157
+ inputEl.addEventListener('input', () => { showSuggestions(); });
158
+
159
+ inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
160
+ if (e.key === 'ArrowDown') {
161
+ e.preventDefault();
162
+ if (menuItems.length > 0) {
163
+ const next = (highlightedIndex + 1) % menuItems.length;
164
+ updateHighlight(next);
165
+ }
166
+ } else if (e.key === 'ArrowUp') {
167
+ e.preventDefault();
168
+ if (menuItems.length > 0) {
169
+ const prev = (highlightedIndex - 1 + menuItems.length) % menuItems.length;
170
+ updateHighlight(prev);
171
+ }
172
+ } else if (e.key === 'Enter') {
173
+ e.preventDefault();
174
+ e.stopPropagation();
175
+ if (highlightedIndex >= 0 && highlightedIndex < menuItems.length) {
176
+ menuItems[highlightedIndex].click();
177
+ } else {
178
+ // If input exactly matches a symbol, add it directly
179
+ const val = inputEl.value.trim();
180
+ if (val && allSymbols.includes(val))
181
+ addMonomer(val);
182
+ }
183
+ } else if (e.key === 'Escape') {
184
+ hideMenu();
185
+ }
186
+ });
187
+
188
+ renderTags();
189
+
190
+ const dlg = ui.dialog({title: 'Select Monomers', showFooter: true})
191
+ .add(ui.div([input.root, tagsHost], {style: {minWidth: '350px', minHeight: '200px'}}))
192
+ .onOK(() => { resolve(selectedMonomers); })
193
+ .onCancel(() => { resolve(null); })
194
+ .show({resizable: true});
195
+ // dlg.root.addEventListener('close', () => { hideMenu(); });
196
+
197
+ inputEl.focus();
198
+ setTimeout(() => { showSuggestions(); }, 0);
199
+ });
200
+ }