@datagrok/sequence-translator 1.10.7 → 1.10.8

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/sequence-translator",
3
3
  "friendlyName": "Sequence Translator",
4
- "version": "1.10.7",
4
+ "version": "1.10.8",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
@@ -147,4 +147,8 @@ export namespace funcs {
147
147
  export async function applyNotationProviderForCyclized(col: DG.Column , separator: string ): Promise<void> {
148
148
  return await grok.functions.call('SequenceTranslator:ApplyNotationProviderForCyclized', { col, separator });
149
149
  }
150
+
151
+ export async function harmonizedSequenceNotationProviderConstructor(): Promise<any> {
152
+ return await grok.functions.call('SequenceTranslator:HarmonizedSequenceNotationProviderConstructor', {});
153
+ }
150
154
  }
@@ -40,3 +40,10 @@ export const PT_UI_DIALOG_CONVERSION = 'Poly Tool Conversion';
40
40
  export const PT_UI_DIALOG_UNRULE = 'Poly Tool Unrule';
41
41
  export const PT_UI_DIALOG_ENUMERATION = 'Poly Tool Enumeration';
42
42
  export const PT_UI_RULES_USED = 'Rules used';
43
+
44
+ export const PT_ENUM_TYPE_TOOLTIPS: Record<string, string> = {
45
+ 'single': 'Each position is enumerated independently. Total results = sum of monomers across all positions.',
46
+ 'parallel': 'The i-th result uses the i-th monomer from every position (zip). All positions must have the same number of monomers. Total results = number of monomers per position.',
47
+ 'matrix': 'Cartesian product of all positions. Total results = product of monomer counts across all positions.',
48
+ 'library': 'Substitutes all monomers from a selected library at a single position.',
49
+ };
@@ -28,7 +28,7 @@ import {PolyToolPlaceholdersInput} from './pt-placeholders-input';
28
28
  import {showMonomerSelectionDialog} from './pt-monomer-selection-dialog';
29
29
  import {defaultErrorHandler} from '../utils/err-info';
30
30
  import {PolyToolPlaceholdersBreadthInput} from './pt-placeholders-breadth-input';
31
- import {PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
31
+ import {PT_ENUM_TYPE_TOOLTIPS, PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
32
32
  import {PolyToolDataRole, PolyToolTags} from '../consts';
33
33
  import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './conversion/pt-rules';
34
34
  import {Chain} from './conversion/pt-chain';
@@ -39,8 +39,22 @@ import {buildMonomerHoverLink} from '@datagrok-libraries/bio/src/monomer-works/m
39
39
  import {getRdKitModule} from '@datagrok-libraries/bio/src/chem/rdkit-module';
40
40
 
41
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';
42
+ import {CyclizedNotationProvider} from '../utils/cyclized';
43
+ import {INotationProvider, NotationProviderBase} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
44
+
45
+ /**
46
+ * PolyTool Enumeration Dialog
47
+ *
48
+ * Provides the UI for enumerating macromolecule variants. The user selects positions
49
+ * on a HELM molecule and specifies substitute monomers at each position. Supports
50
+ * multiple enumeration strategies (Single, Parallel, Matrix, Library) and optional
51
+ * downstream processing (atomic-level conversion, chirality, rules).
52
+ *
53
+ * Architecture:
54
+ * polyToolEnumerateHelmUI() - Entry point: creates, sizes, and shows the dialog
55
+ * getPolyToolEnumerateDialog() - Builds all inputs, validators, event wiring, and the dialog
56
+ * polyToolEnumerateSeq() - Executes enumeration and post-processes results into a DataFrame
57
+ */
44
58
 
45
59
  type PolyToolEnumerateInputs = {
46
60
  macromolecule: HelmInputBase;
@@ -73,15 +87,21 @@ type PolyToolEnumerateHelmSerialized = {
73
87
  library: string;
74
88
  };
75
89
 
90
+ /** Entry point: creates, sizes, and shows the enumeration dialog. */
76
91
  export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
77
92
  await _package.initPromise;
78
93
 
94
+ // Capture viewport dimensions for dialog sizing
79
95
  const maxWidth = window.innerWidth;
80
96
  const maxHeight = window.innerHeight;
81
97
 
82
98
  try {
83
99
  // eslint-disable-next-line prefer-const
84
100
  let dialog: DG.Dialog;
101
+
102
+ // Dynamically allocates remaining vertical space to flex-fit inputs (e.g. the macromolecule editor)
103
+ // after fixed-height inputs are laid out. fitInputs maps child element indices to proportional
104
+ // height weights; elements not in fitInputs get their natural height.
85
105
  function resizeInputs() {
86
106
  if (dialog == null) return;
87
107
 
@@ -107,18 +127,13 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
107
127
  };
108
128
  dialog = await getPolyToolEnumerateDialog(cell, resizeInputs);
109
129
 
130
+ // On first show, center the dialog at 70% of viewport; on subsequent resizes, just reflow inputs
110
131
  let isFirstShow = true;
111
132
  ui.onSizeChanged(dialog.root).subscribe(() => {
112
133
  if (isFirstShow) {
113
- // const dialogRootCash = $(dialog.root);
114
- // const contentMaxHeight = maxHeight -
115
- // dialogRootCash.find('div.d4-dialog-header').get(0)!.offsetHeight -
116
- // dialogRootCash.find('div.d4-dialog-footer').get(0)!.offsetHeight;
117
-
118
134
  const dialogWidth = maxWidth * 0.7;
119
135
  const dialogHeight = maxHeight * 0.7;
120
136
 
121
- // Centered, but resizable dialog
122
137
  dialog.root.style.width = `${Math.min(maxWidth, dialogWidth)}px`;
123
138
  dialog.root.style.height = `${Math.min(maxHeight, dialogHeight)}px`;
124
139
  dialog.root.style.left = `${Math.floor((maxWidth - dialog.root.offsetWidth) / 2)}px`;
@@ -141,19 +156,21 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
141
156
  }
142
157
  }
143
158
 
159
+ /** Builds and configures the enumeration dialog with all inputs, validators, and event handlers. */
144
160
  async function getPolyToolEnumerateDialog(
145
161
  cell?: DG.Cell, resizeInputs?: () => void
146
162
  ): Promise<DG.Dialog> {
147
163
  const logPrefix = `ST: PT: HelmDialog()`;
148
164
  let inputs: PolyToolEnumerateInputs;
149
165
  const subs: Unsubscribable[] = [];
150
- // will store previous enumeration type to support logic of cleanups
166
+ // Track previous enumeration type to clean up state when switching to/from Library mode
151
167
  let prevEnumerationType: PolyToolEnumeratorType = PolyToolEnumeratorTypes.Single;
152
168
  const destroy = () => {
153
169
  for (const sub of subs) sub.unsubscribe();
154
170
  inputs.placeholders.detach();
155
171
  };
156
172
  try {
173
+ // --- Initialize helpers (monomer lib, sequence helper, HELM helper) ---
157
174
  const libHelper = await getMonomerLibHelper();
158
175
  const monomerLib = libHelper.getMonomerLib();
159
176
  const seqHelper = await getSeqHelper();
@@ -162,6 +179,8 @@ async function getPolyToolEnumerateDialog(
162
179
  const [_libList, helmHelper] = await Promise.all([getLibrariesList(), getHelmHelper()]);
163
180
  const monomerLibFuncs = helmHelper.buildMonomersFuncsFromLib(monomerLib);
164
181
 
182
+ // Resolves the source macromolecule from the given cell or falls back to a default example.
183
+ // Returns the sequence value and its data role (macromolecule vs template).
165
184
  const getValue = (cell?: DG.Cell): [SeqValueBase, PolyToolDataRole] => {
166
185
  let resSeqValue: SeqValueBase;
167
186
  let resDataRole: PolyToolDataRole;
@@ -197,12 +216,14 @@ async function getPolyToolEnumerateDialog(
197
216
 
198
217
  let [seqValue, dataRole]: [SeqValueBase, PolyToolDataRole] = getValue(cell);
199
218
 
219
+ // --- UI state ---
200
220
  let srcId: { value: string, colName: string } | null = null;
201
221
  let ruleFileList: string[];
202
222
  let ruleInputs: RuleInputs;
203
223
  const trivialNameSampleDiv = ui.divText('', {style: {marginLeft: '8px', marginTop: '2px'}});
204
224
  const warningsTextDiv = ui.divText('', {style: {color: 'red'}});
205
- // #### Inputs
225
+
226
+ // === INPUT DEFINITIONS ===
206
227
  inputs = {
207
228
  macromolecule: helmHelper.createHelmInput(
208
229
  'Macromolecule', {
@@ -292,6 +313,14 @@ async function getPolyToolEnumerateDialog(
292
313
  library: ui.input.choice('Monomer Library', {items: _libList, value: _libList[0], nullable: true}) as DG.InputBase<string>,
293
314
  };
294
315
  // #### Inputs END
316
+
317
+ // Bind tooltip to enumerator type choice, updating on each change
318
+ const updateEnumTypeTooltip = () => {
319
+ const tooltipText = PT_ENUM_TYPE_TOOLTIPS[inputs.enumeratorType.value] ?? '';
320
+ ui.tooltip.bind(inputs.enumeratorType.input, tooltipText);
321
+ };
322
+ updateEnumTypeTooltip();
323
+
295
324
  inputs.library.root.style.setProperty('display', 'none');
296
325
  inputs.trivialNameCol.addOptions(trivialNameSampleDiv);
297
326
 
@@ -319,23 +348,41 @@ async function getPolyToolEnumerateDialog(
319
348
  return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
320
349
  };
321
350
 
351
+ // === VALIDATORS ===
352
+ // Validates placeholder positions and monomers based on the current enumeration mode.
322
353
  let placeholdersValidity: string | null = null;
323
354
  inputs.placeholders.addValidator((value: string): string | null => {
355
+ placeholdersValidity = null;
324
356
  const errors: string[] = [];
357
+ setTimeout(() => { updateWarnings(); }, 100);
325
358
  try {
326
359
  // for library, ony one placeholder is allowed
327
360
  if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
328
- setTimeout(() => { updateWarnings(); }, 10);
329
361
  const pcs = inputs.placeholders.placeholdersValue;
330
362
  if (pcs.length > 1)
331
- return 'Only one placeholder is allowed for Library mode';
363
+ placeholdersValidity = 'Only one placeholder is allowed for Library mode';
332
364
  if (pcs.length === 1) {
333
365
  if (pcs[0].position == null)
334
- return 'Position is required for Library mode';
366
+ placeholdersValidity = 'Position is required for Library mode';
335
367
  if (pcs[0].position > inputs.macromolecule.molValue.atoms.length)
336
- return `There is no monomer at position ${pcs[0].position + 1}.`;
368
+ placeholdersValidity = `There is no monomer at position ${pcs[0].position + 1}.`;
369
+ }
370
+ return placeholdersValidity;
371
+ }
372
+
373
+ // Parallel mode: all placeholders must have the same monomer count
374
+ if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Parallel) {
375
+ const pcs = inputs.placeholders.placeholdersValue;
376
+ const nonEmpty = pcs.filter((ph) => ph.monomers.length > 0);
377
+ if (nonEmpty.length > 1) {
378
+ const firstCount = nonEmpty[0].monomers.length;
379
+ const mismatch = nonEmpty.find((ph) => ph.monomers.length !== firstCount);
380
+ if (mismatch) {
381
+ placeholdersValidity = `Parallel mode requires all positions to have the same number of monomers. ` +
382
+ `Position ${nonEmpty[0].position + 1} has ${firstCount}, ` +
383
+ `but position ${mismatch.position + 1} has ${mismatch.monomers.length}.`;
384
+ }
337
385
  }
338
- return null;
339
386
  }
340
387
 
341
388
  if (dataRole !== PolyToolDataRole.macromolecule)
@@ -373,7 +420,7 @@ async function getPolyToolEnumerateDialog(
373
420
  .join('\n');
374
421
  if (Object.keys(byTypeStr).length > 0)
375
422
  errors.push(`Placeholders contain missed monomers: ${byTypeStr}`);
376
- placeholdersValidity = errors.length > 0 ? errors.join('\n') : null;
423
+ placeholdersValidity = errors.length > 0 ? errors.join('\n') : placeholdersValidity;
377
424
  } catch (err: any) {
378
425
  const [errMsg, errStack] = defaultErrorHandler(err, false);
379
426
  placeholdersValidity = errMsg;
@@ -460,8 +507,14 @@ async function getPolyToolEnumerateDialog(
460
507
  defaultErrorHandler(err, false);
461
508
  }
462
509
  prevEnumerationType = inputs.enumeratorType.value;
510
+ updateEnumTypeTooltip();
511
+ // Re-validate placeholders (Parallel mode has different constraints than Single/Matrix)
512
+ inputs.placeholders.fireChanged();
463
513
  });
464
514
 
515
+ // === MOLECULE INTERACTION EVENT HANDLERS ===
516
+
517
+ // Mouse move: show tooltip of substitute monomers at hovered position
465
518
  subs.push(inputs.macromolecule.onMouseMove.subscribe((e: MouseEvent) => {
466
519
  try {
467
520
  _package.logger.debug(`${logPrefix}, placeholdersInput.onMouseMove()`);
@@ -486,6 +539,7 @@ async function getPolyToolEnumerateDialog(
486
539
  defaultErrorHandler(err, false);
487
540
  }
488
541
  }));
542
+ // Click on molecule: add clicked atom position to the placeholders grid
489
543
  subs.push(inputs.macromolecule.onClick.subscribe((e: MouseEvent) => {
490
544
  try {
491
545
  _package.logger.debug(`${logPrefix}, placeholdersInput.onClick()`);
@@ -528,6 +582,9 @@ async function getPolyToolEnumerateDialog(
528
582
  inputs.macromolecule.root.style.setProperty('min-width', '250px', 'important');
529
583
  // inputs.macromolecule.root.style.setProperty('max-height', '300px', 'important');
530
584
 
585
+ // === VIEW UPDATE HELPERS ===
586
+
587
+ // Highlights atoms in the molecule editor that have placeholders defined
531
588
  const updateViewMol = () => {
532
589
  const phPosSet = new Set<number>(inputs.placeholders.placeholdersValue.map((ph) => ph.position));
533
590
  const mol = inputs.macromolecule.molValue;
@@ -538,6 +595,7 @@ async function getPolyToolEnumerateDialog(
538
595
  inputs.macromolecule.redraw();
539
596
  };
540
597
 
598
+ // Shows/hides conversion rules UI based on toAtomicLevel setting and data role
541
599
  const updateViewRules = () => {
542
600
  if (inputs.toAtomicLevel.value && dataRole === PolyToolDataRole.template) {
543
601
  inputs.generateHelm.root.style.removeProperty('display');
@@ -553,7 +611,7 @@ async function getPolyToolEnumerateDialog(
553
611
  if (resizeInputs)
554
612
  resizeInputs();
555
613
  };
556
-
614
+ let dialogOKButton: HTMLButtonElement | null | undefined = null;
557
615
  const updateWarnings = () => {
558
616
  const warnings = placeholdersValidity;
559
617
  // const iw = inputs.warnings;
@@ -565,6 +623,8 @@ async function getPolyToolEnumerateDialog(
565
623
 
566
624
  w.innerText = warnings;
567
625
  w.style.removeProperty('display');
626
+ if (dialogOKButton)
627
+ dialogOKButton.disabled = true;
568
628
  } else {
569
629
  // iw.value = ''; // <- breaks dialog resize
570
630
  // iw.enabled = false;
@@ -572,6 +632,8 @@ async function getPolyToolEnumerateDialog(
572
632
 
573
633
  w.innerText = '';
574
634
  w.style.setProperty('display', 'none');
635
+ if (dialogOKButton)
636
+ dialogOKButton.disabled = false;
575
637
  }
576
638
  //resizeInputs();
577
639
  };
@@ -597,6 +659,8 @@ async function getPolyToolEnumerateDialog(
597
659
  fillForCurrentCell(seqValue, dataRole, cell);
598
660
  updateViewRules();
599
661
 
662
+ // === EXECUTION (OK button handler) ===
663
+ // Pre-flight validates inputs, resolves Library mode, builds params, and runs enumeration.
600
664
  const exec = async (): Promise<void> => {
601
665
  try {
602
666
  const srcHelm = inputs.macromolecule.stringValue;
@@ -625,6 +689,7 @@ async function getPolyToolEnumerateDialog(
625
689
  await getHelmHelper(); // initializes JSDraw and org
626
690
 
627
691
  const placeHoldersValue = inputs.placeholders.placeholdersValue;
692
+ // Library mode: load all monomers from the selected library and convert to Single mode
628
693
  let enumerationType = inputs.enumeratorType.value;
629
694
  if (enumerationType === PolyToolEnumeratorTypes.Library) {
630
695
  if (placeHoldersValue.length !== 1) {
@@ -647,12 +712,33 @@ async function getPolyToolEnumerateDialog(
647
712
  enumerationType = PolyToolEnumeratorTypes.Single;
648
713
  }
649
714
 
715
+ if (enumerationType === PolyToolEnumeratorTypes.Parallel) {
716
+ const nonEmpty = placeHoldersValue.filter((ph) => ph.monomers.length > 0);
717
+ if (nonEmpty.length > 1) {
718
+ const firstCount = nonEmpty[0].monomers.length;
719
+ if (nonEmpty.some((ph) => ph.monomers.length !== firstCount)) {
720
+ grok.shell.warning('Parallel mode requires all positions to have the same number of monomers');
721
+ return;
722
+ }
723
+ }
724
+ }
725
+
650
726
  const params: PolyToolEnumeratorParams = {
651
727
  placeholders: placeHoldersValue,
652
728
  type: enumerationType,
653
729
  breadthPlaceholders: inputs.placeholdersBreadth.placeholdersBreadthValue,
654
730
  keepOriginal: inputs.keepOriginal.value,
655
731
  };
732
+
733
+ if (cell?.column?.temp?.[SeqTemps.notationProvider] &&
734
+ !(cell.column.temp[SeqTemps.notationProvider] instanceof CyclizedNotationProvider)) {
735
+ const notationProvider = cell.column.temp[SeqTemps.notationProvider] as INotationProvider;
736
+ if ((notationProvider.constructor as typeof NotationProviderBase).implementsFromHelm) {
737
+ const cons = notationProvider.constructor as typeof NotationProviderBase;
738
+ params.fromHelmNotation = {notationName: cons.notationName, convert: (helm) => cons.convertFromHelm(helm, {})};
739
+ }
740
+ }
741
+
656
742
  const toAtomicLevelV: boolean = inputs.toAtomicLevel.value;
657
743
  const enumeratorResDf = await polyToolEnumerateSeq(srcHelm, dataRole, srcId, params,
658
744
  toAtomicLevelV ? {
@@ -669,6 +755,8 @@ async function getPolyToolEnumerateDialog(
669
755
  }
670
756
  };
671
757
 
758
+ // === DIALOG CONSTRUCTION AND LAYOUT ===
759
+ // Layout: macromolecule editor on top, two-column (placeholders | breadth), two-column (options | rules)
672
760
  const dialog = ui.dialog({title: PT_UI_DIALOG_ENUMERATION, showFooter: true})
673
761
  .add(inputs.macromolecule.root)
674
762
  .add(ui.divH([
@@ -693,14 +781,20 @@ async function getPolyToolEnumerateDialog(
693
781
  {style: {width: '50%'}})],
694
782
  {style: {width: '100%'}}))
695
783
  .add(warningsTextDiv)
696
- // .addButton('Enumerate', () => {
697
- // execDialog()
698
- // .then(() => {});
699
- // }, 0, 'Keeps the dialog open')
700
- .onOK(() => { exec(); });
784
+ .onOK(() => {
785
+ if (placeholdersValidity) {
786
+ updateWarnings();
787
+ grok.shell.warning('Please fix validation errors before running enumeration');
788
+ return;
789
+ }
790
+ exec();
791
+ });
792
+ // .onOK(() => { exec(); });
793
+ dialogOKButton = dialog.getButton('OK') as HTMLButtonElement;
701
794
  subs.push(dialog.onClose.subscribe(() => {
702
795
  destroy();
703
796
  }));
797
+ // Dialog history: serialization/deserialization for persisting dialog state across sessions
704
798
  dialog.history(
705
799
  /* getInput */ (): PolyToolEnumerateHelmSerialized => {
706
800
  return {
@@ -732,6 +826,10 @@ async function getPolyToolEnumerateDialog(
732
826
  inputs.highlightMonomers.value = x.highlightMonomers ?? false;
733
827
  ruleInputs.setActive(x.rules);
734
828
  inputs.library.value = x.library;
829
+ setTimeout(() => {
830
+ inputs.placeholders.invalidateGrid();
831
+ inputs.placeholdersBreadth.invalidateGrid();
832
+ }, 100);
735
833
  });
736
834
  return dialog;
737
835
  } catch (err: any) {
@@ -740,10 +838,16 @@ async function getPolyToolEnumerateDialog(
740
838
  }
741
839
  }
742
840
 
743
- /**
744
- * @param {DG.SemanticValue} srcValue Source value to enumerate, either of data role
745
- * {@link PolyToolDataRole.template} or {@link PolyToolDataRole.macromolecule}
746
- * */
841
+ /** Executes enumeration and post-processes results into a DataFrame.
842
+ * Handles both macromolecule and template data roles, with optional
843
+ * atomic-level conversion (chirality, highlighting, rules).
844
+ *
845
+ * @param srcHelm Source HELM string to enumerate
846
+ * @param dataRole Whether the source is a {@link PolyToolDataRole.template} or {@link PolyToolDataRole.macromolecule}
847
+ * @param srcId Optional trivial name column info for generating IDs
848
+ * @param params Enumeration parameters (type, placeholders, keepOriginal)
849
+ * @param toAtomicLevel Post-processing options, or false to skip
850
+ */
747
851
  export async function polyToolEnumerateSeq(
748
852
  srcHelm: string, dataRole: PolyToolDataRole, srcId: { value: string, colName: string } | null,
749
853
  params: PolyToolEnumeratorParams,
@@ -756,7 +860,10 @@ export async function polyToolEnumerateSeq(
756
860
  const rdKitModule = await getRdKitModule();
757
861
  const monomerLib = libHelper.getMonomerLib(); // TODO: Get monomer lib from src SeqValueBase
758
862
 
863
+ // Core enumeration: produces [helm, id] pairs
759
864
  const resList = doPolyToolEnumerateHelm(srcHelm, srcId?.value ?? '', params);
865
+
866
+ // Create result column based on data role (macromolecule uses HELM directly, template converts via Chain)
760
867
  let enumCol: DG.Column<string>;
761
868
  switch (dataRole) {
762
869
  case PolyToolDataRole.macromolecule: {
@@ -779,11 +886,19 @@ export async function polyToolEnumerateSeq(
779
886
  }
780
887
  }
781
888
  const enumeratorResDf = DG.DataFrame.fromColumns([enumCol]);
889
+
890
+ if (dataRole === PolyToolDataRole.macromolecule && params.fromHelmNotation) {
891
+ const c = DG.Column.fromType(DG.COLUMN_TYPE.STRING, `${params.fromHelmNotation.notationName}(${enumCol.name})`, enumeratorResDf.rowCount)
892
+ .init((rowIdx: number) => enumCol.isNone(rowIdx) ? null : params.fromHelmNotation!.convert(enumCol.get(rowIdx)!));
893
+ enumeratorResDf.columns.add(c);
894
+ }
895
+
782
896
  await grok.data.detectSemanticTypes(enumeratorResDf);
783
897
  if (dataRole == PolyToolDataRole.template)
784
898
  PackageFunctions.applyNotationProviderForCyclized(enumCol, '-');
785
899
 
786
900
 
901
+ // Optional post-processing: convert to atomic level with chirality/highlighting
787
902
  if (toAtomicLevel) {
788
903
  let resHelmCol: DG.Column<string>;
789
904
  if (dataRole === PolyToolDataRole.macromolecule) {
@@ -800,6 +915,7 @@ export async function polyToolEnumerateSeq(
800
915
  }
801
916
  }
802
917
 
918
+ // Add trivial name ID column if the source had one
803
919
  if (srcId) {
804
920
  const enumIdCol = DG.Column.fromType(DG.COLUMN_TYPE.STRING, srcId.colName, resList.length)
805
921
  .init((rowIdx: number) => resList[rowIdx][1]);
@@ -61,6 +61,39 @@ function getPtEnumeratorMatrix(m: HelmMol, placeholders: PolyToolPlaceholder[]):
61
61
  return resMolList;
62
62
  }
63
63
 
64
+ /** Parallel (zip) enumeration: the i-th result takes the i-th monomer from each placeholder position.
65
+ * All placeholders must have the same number of monomers (validated upstream).
66
+ * With K positions and N monomers each, produces exactly N results. */
67
+ function getPtEnumeratorParallel(m: HelmMol, placeholders: PolyToolPlaceholder[]): HelmMol[] {
68
+ if (placeholders.length === 0)
69
+ return [];
70
+
71
+ const monomerCount = placeholders[0].monomers.length;
72
+ for (const ph of placeholders) {
73
+ if (ph.monomers.length !== monomerCount)
74
+ throw new Error(`Parallel enumeration requires all positions to have the same number of monomers`);
75
+ }
76
+
77
+ const resMolList: HelmMol[] = new Array<HelmMol>(monomerCount);
78
+ for (let i = 0; i < monomerCount; i++) {
79
+ const resM = m.clone() as HelmMol;
80
+ const nameParts: string[] = [];
81
+ for (const ph of placeholders) {
82
+ const pos = ph.position;
83
+ const newSymbol = ph.monomers[i];
84
+ const oldSymbol = resM.atoms[pos].elem;
85
+ resM.atoms[pos].elem = newSymbol;
86
+
87
+ const idOldSymbol = oldSymbol?.length > 1 ? `[${oldSymbol}]` : oldSymbol;
88
+ const idNewSymbol = newSymbol?.length > 1 ? `[${newSymbol}]` : newSymbol;
89
+ nameParts.push(`${idOldSymbol}${pos + 1}${idNewSymbol}`);
90
+ }
91
+ resM.name = `${m.name}-${nameParts.join('-')}`;
92
+ resMolList[i] = resM;
93
+ }
94
+ return resMolList;
95
+ }
96
+
64
97
  function getPtEnumeratorBreadth(m: HelmMol, placeholdersBreadth: PolyToolBreadthPlaceholder[]): HelmMol[] {
65
98
  if (placeholdersBreadth.length == 0)
66
99
  return [];
@@ -90,6 +123,10 @@ export function doPolyToolEnumerateHelm(
90
123
  resMolList = getPtEnumeratorSingle(molHandler.m, params.placeholders);
91
124
  break;
92
125
  }
126
+ case PolyToolEnumeratorTypes.Parallel: {
127
+ resMolList = getPtEnumeratorParallel(molHandler.m, params.placeholders);
128
+ break;
129
+ }
93
130
  case PolyToolEnumeratorTypes.Matrix: {
94
131
  resMolList = getPtEnumeratorMatrix(molHandler.m, params.placeholders);
95
132
  break;
@@ -6,6 +6,8 @@ import {HelmType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
6
6
  import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types/monomer-library';
7
7
  import {polymerTypeToHelmType} from '@datagrok-libraries/bio/src/utils/macromolecule/utils';
8
8
 
9
+ import {parseMonomerSymbolList} from './pt-placeholders-input';
10
+
9
11
  const MAX_SUGGESTIONS = 20;
10
12
 
11
13
  /** Shows a dialog for selecting monomers with autocomplete and tag-based display.
@@ -156,6 +158,41 @@ export async function showMonomerSelectionDialog(
156
158
 
157
159
  inputEl.addEventListener('input', () => { showSuggestions(); });
158
160
 
161
+ // Handle pasting multiple monomers (space / comma / newline separated)
162
+ inputEl.addEventListener('paste', (e: ClipboardEvent) => {
163
+ const pastedText = e.clipboardData?.getData('text');
164
+ if (!pastedText)
165
+ return;
166
+
167
+ // Split on newlines first, then parse each line (handles parenthesized symbols like hArg(Et,Et))
168
+ const lines = pastedText.split(/[\n\r]+/).map((l) => l.trim()).filter((l) => l.length > 0);
169
+ const candidates: string[] = [];
170
+ for (const line of lines) {
171
+ const parsed = parseMonomerSymbolList(line);
172
+ // If parseMonomerSymbolList returned a single token but the line has spaces and no commas,
173
+ // split on spaces as well (e.g. "K P F" -> ["K", "P", "F"])
174
+ if (parsed.length === 1 && line.includes(' ') && !line.includes(','))
175
+ candidates.push(...line.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0));
176
+ else
177
+ candidates.push(...parsed);
178
+ }
179
+
180
+ if (candidates.length <= 1)
181
+ return; // Single symbol: let default paste + autocomplete handle it
182
+
183
+ e.preventDefault();
184
+
185
+ for (const candidate of candidates) {
186
+ if (allSymbols.includes(candidate) && !selectedMonomers.includes(candidate))
187
+ selectedMonomers.push(candidate);
188
+ }
189
+
190
+ renderTags();
191
+ inputEl.value = '';
192
+ hideMenu();
193
+ inputEl.focus();
194
+ });
195
+
159
196
  inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
160
197
  if (e.key === 'ArrowDown') {
161
198
  e.preventDefault();
@@ -29,6 +29,19 @@ export class PolyToolPlaceholdersBreadthInput extends DG.JsInputBase<DG.DataFram
29
29
  this.setDataFrame(value);
30
30
  }
31
31
 
32
+ public invalidateGrid(): void {
33
+ if (this.grid) {
34
+ const oldW = this.grid.root.style.width;
35
+ this.grid.root.style.width = '99.9%';
36
+ setTimeout(() => {
37
+ if (oldW)
38
+ this.grid.root.style.width = oldW;
39
+ else
40
+ this.grid.root.style.removeProperty('width');
41
+ }, 100);
42
+ }
43
+ }
44
+
32
45
  getStringValue(): string { return this.grid.dataFrame.toCsv(); }
33
46
 
34
47
  setStringValue(str: string): void {
@@ -52,6 +52,19 @@ export class PolyToolPlaceholdersInput extends DG.JsInputBase<DG.DataFrame> {
52
52
 
53
53
  private subs: Unsubscribable[] = [];
54
54
 
55
+ public invalidateGrid(): void {
56
+ if (this.grid) {
57
+ const oldW = this.grid.root.style.width;
58
+ this.grid.root.style.width = '99.9%';
59
+ setTimeout(() => {
60
+ if (oldW)
61
+ this.grid.root.style.width = oldW;
62
+ else
63
+ this.grid.root.style.removeProperty('width');
64
+ }, 100);
65
+ }
66
+ }
67
+
55
68
  protected constructor(
56
69
  private readonly name: string | undefined,
57
70
  heightRowCount?: number, options?: {},
@@ -6,6 +6,7 @@ import {PolymerType} from '@datagrok-libraries/bio/src/helm/types';
6
6
 
7
7
  export enum PolyToolEnumeratorTypes {
8
8
  Single = 'single',
9
+ Parallel = 'parallel',
9
10
  Matrix = 'matrix',
10
11
  Library = 'library',
11
12
  }
@@ -27,6 +28,7 @@ export type PolyToolEnumeratorParams = {
27
28
  breadthPlaceholders?: PolyToolBreadthPlaceholder[];
28
29
  keepOriginal?: boolean;
29
30
  trivialName?: boolean;
31
+ fromHelmNotation?: {convert:(helm: string) => string; notationName: string};
30
32
  }
31
33
 
32
34
  export class MonomerNotFoundError extends Error {
@@ -73,6 +73,66 @@ category('PolyTool: Enumerate', () => {
73
73
  {seq: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-[Tic]7T'},
74
74
  ]
75
75
  },
76
+ 'parallel1': {
77
+ src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
78
+ params: {
79
+ type: PolyToolEnumeratorTypes.Parallel,
80
+ placeholders: [
81
+ {position: 1, monomers: ['D', 'L']},
82
+ {position: 6, monomers: ['Y', 'T']},
83
+ ],
84
+ },
85
+ tgt: [
86
+ {seq: 'PEPTIDE1{[Ac(1)].D.W.G.P.L.Y.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2D-[Tic]7Y'},
87
+ {seq: 'PEPTIDE1{[Ac(1)].L.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2L-[Tic]7T'},
88
+ ],
89
+ },
90
+ 'parallel-three-positions': {
91
+ src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
92
+ params: {
93
+ type: PolyToolEnumeratorTypes.Parallel,
94
+ placeholders: [
95
+ {position: 1, monomers: ['D', 'L', 'K']},
96
+ {position: 4, monomers: ['K', 'P', 'F4COO']},
97
+ {position: 6, monomers: ['Y', 'T', 'A']},
98
+ ],
99
+ },
100
+ tgt: [
101
+ {seq: 'PEPTIDE1{[Ac(1)].D.W.G.K.L.Y.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2D-P5K-[Tic]7Y'},
102
+ {seq: 'PEPTIDE1{[Ac(1)].L.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2L-P5P-[Tic]7T'},
103
+ {seq: 'PEPTIDE1{[Ac(1)].K.W.G.[F4COO].L.A.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2K-P5[F4COO]-[Tic]7A'},
104
+ ],
105
+ },
106
+ 'parallel-with-original': {
107
+ src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
108
+ params: {
109
+ type: PolyToolEnumeratorTypes.Parallel,
110
+ placeholders: [
111
+ {position: 1, monomers: ['D', 'L']},
112
+ {position: 6, monomers: ['Y', 'T']},
113
+ ],
114
+ keepOriginal: true,
115
+ },
116
+ tgt: [
117
+ {seq: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: ''},
118
+ {seq: 'PEPTIDE1{[Ac(1)].D.W.G.P.L.Y.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2D-[Tic]7Y'},
119
+ {seq: 'PEPTIDE1{[Ac(1)].L.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2L-[Tic]7T'},
120
+ ],
121
+ },
122
+ 'parallel-single-position': {
123
+ src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
124
+ params: {
125
+ type: PolyToolEnumeratorTypes.Parallel,
126
+ placeholders: [
127
+ {position: 4, monomers: ['K', 'P', 'F4COO']},
128
+ ],
129
+ },
130
+ tgt: [
131
+ {seq: 'PEPTIDE1{[Ac(1)].F.W.G.K.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: '-P5K'},
132
+ {seq: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: '-P5P'},
133
+ {seq: 'PEPTIDE1{[Ac(1)].F.W.G.[F4COO].L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: '-P5[F4COO]'},
134
+ ],
135
+ },
76
136
  'matrix1': {
77
137
  src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
78
138
  params: