@datagrok/sequence-translator 1.10.7 → 1.10.9

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.
@@ -3,15 +3,19 @@ import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
5
  import {HelmType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
6
- import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types/monomer-library';
6
+ import {IMonomerLib, IMonomerLibHelper, 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
- /** Shows a dialog for selecting monomers with autocomplete and tag-based display.
12
- * @returns comma-separated monomer symbols, or null if cancelled */
13
+ /** Shows a dialog for selecting monomers with autocomplete, tag-based display,
14
+ * and bulk-add from monomer libraries or collections.
15
+ * @returns monomer symbols array, or null if cancelled */
13
16
  export async function showMonomerSelectionDialog(
14
17
  monomerLib: IMonomerLib, polymerType: PolymerType, presetMonomers?: string[],
18
+ libHelper?: IMonomerLibHelper,
15
19
  ): Promise<string[] | null> {
16
20
  return new Promise<string[] | null>((resolve) => {
17
21
  const helmType: HelmType = polymerTypeToHelmType(polymerType);
@@ -19,11 +23,19 @@ export async function showMonomerSelectionDialog(
19
23
 
20
24
  const selectedMonomers: string[] = presetMonomers ? [...presetMonomers] : [];
21
25
 
22
- const tagsHost = ui.div([], {style: {display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '8px', maxWidth: '400px'}});
23
- const input = ui.input.string('Monomers', {value: ''});
26
+ // --- Tags display (selected monomers) ---
27
+ const tagsHost = ui.div([], {style: {display: 'flex', flexWrap: 'wrap', gap: '4px', marginTop: '4px', maxHeight: '150px', overflowY: 'auto'}});
28
+ const countLabel = ui.divText('', {style: {fontSize: '11px', color: 'var(--grey-4)', marginTop: '4px'}});
29
+
30
+ function updateCountLabel(): void {
31
+ countLabel.textContent = selectedMonomers.length > 0 ? `${selectedMonomers.length} monomer(s) selected` : '';
32
+ }
33
+
34
+ // --- Autocomplete input ---
35
+ const input = ui.input.string('Search', {value: ''});
24
36
  const inputEl = input.input as HTMLInputElement;
25
37
  inputEl.setAttribute('autocomplete', 'off');
26
- inputEl.placeholder = 'Type to search...';
38
+ inputEl.placeholder = 'Type to search monomers...';
27
39
 
28
40
  let currentMenu: DG.Menu | null = null;
29
41
  let menuItems: HTMLElement[] = [];
@@ -37,6 +49,7 @@ export async function showMonomerSelectionDialog(
37
49
  if (idx >= 0) {
38
50
  selectedMonomers.splice(idx, 1);
39
51
  renderTags();
52
+ updateCountLabel();
40
53
  }
41
54
  });
42
55
  removeBtn.style.marginLeft = '4px';
@@ -51,7 +64,7 @@ export async function showMonomerSelectionDialog(
51
64
  },
52
65
  });
53
66
  // Tooltip on hover
54
- tag.addEventListener('mouseenter', (e) => {
67
+ tag.addEventListener('mouseenter', () => {
55
68
  const tooltip = monomerLib.getTooltip(helmType, symbol);
56
69
  ui.tooltip.show(tooltip, tag.getBoundingClientRect().left, tag.getBoundingClientRect().bottom + 16);
57
70
  });
@@ -59,6 +72,7 @@ export async function showMonomerSelectionDialog(
59
72
 
60
73
  tagsHost.appendChild(tag);
61
74
  }
75
+ updateCountLabel();
62
76
  }
63
77
 
64
78
  function addMonomer(symbol: string): void {
@@ -71,6 +85,18 @@ export async function showMonomerSelectionDialog(
71
85
  inputEl.focus();
72
86
  }
73
87
 
88
+ function addMonomers(symbols: string[]): void {
89
+ let added = false;
90
+ for (const symbol of symbols) {
91
+ if (!selectedMonomers.includes(symbol)) {
92
+ selectedMonomers.push(symbol);
93
+ added = true;
94
+ }
95
+ }
96
+ if (added)
97
+ renderTags();
98
+ }
99
+
74
100
  function hideMenu(): void {
75
101
  if (currentMenu) {
76
102
  currentMenu.hide();
@@ -156,6 +182,41 @@ export async function showMonomerSelectionDialog(
156
182
 
157
183
  inputEl.addEventListener('input', () => { showSuggestions(); });
158
184
 
185
+ // Handle pasting multiple monomers (space / comma / newline separated)
186
+ inputEl.addEventListener('paste', (e: ClipboardEvent) => {
187
+ const pastedText = e.clipboardData?.getData('text');
188
+ if (!pastedText)
189
+ return;
190
+
191
+ // Split on newlines first, then parse each line (handles parenthesized symbols like hArg(Et,Et))
192
+ const lines = pastedText.split(/[\n\r]+/).map((l) => l.trim()).filter((l) => l.length > 0);
193
+ const candidates: string[] = [];
194
+ for (const line of lines) {
195
+ const parsed = parseMonomerSymbolList(line);
196
+ // If parseMonomerSymbolList returned a single token but the line has spaces and no commas,
197
+ // split on spaces as well (e.g. "K P F" -> ["K", "P", "F"])
198
+ if (parsed.length === 1 && line.includes(' ') && !line.includes(','))
199
+ candidates.push(...line.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0));
200
+ else
201
+ candidates.push(...parsed);
202
+ }
203
+
204
+ if (candidates.length <= 1)
205
+ return; // Single symbol: let default paste + autocomplete handle it
206
+
207
+ e.preventDefault();
208
+
209
+ for (const candidate of candidates) {
210
+ if (allSymbols.includes(candidate) && !selectedMonomers.includes(candidate))
211
+ selectedMonomers.push(candidate);
212
+ }
213
+
214
+ renderTags();
215
+ inputEl.value = '';
216
+ hideMenu();
217
+ inputEl.focus();
218
+ });
219
+
159
220
  inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
160
221
  if (e.key === 'ArrowDown') {
161
222
  e.preventDefault();
@@ -185,14 +246,105 @@ export async function showMonomerSelectionDialog(
185
246
  }
186
247
  });
187
248
 
249
+ // --- Bulk add: from monomer library ---
250
+ const librarySection = ui.div([], {style: {marginTop: '8px'}});
251
+ if (libHelper) {
252
+ const libraryInput = ui.input.choice('Add from library', {items: [] as string[], nullable: true}) as DG.ChoiceInput<string>;
253
+ const libraryStatus = ui.divText('', {style: {fontSize: '11px', color: 'var(--green-2)', marginLeft: '8px', minHeight: '16px'}});
254
+
255
+ // Load library names asynchronously
256
+ libHelper.getAvaliableLibraryNames().then((libNames) => {
257
+ libraryInput.items = libNames;
258
+ });
259
+
260
+ libraryInput.onChanged.subscribe(async () => {
261
+ const libName = libraryInput.value;
262
+ if (!libName)
263
+ return;
264
+
265
+ libraryStatus.textContent = 'Loading...';
266
+ try {
267
+ const lib = await libHelper.readSingleLibraryByName(libName);
268
+ if (lib) {
269
+ const symbols = lib.getMonomerSymbolsByType(polymerType);
270
+ const before = selectedMonomers.length;
271
+ addMonomers(symbols);
272
+ const added = selectedMonomers.length - before;
273
+ libraryStatus.textContent = `Added ${added} monomer(s) from "${libName}"`;
274
+ } else {
275
+ libraryStatus.textContent = `Library "${libName}" not found`;
276
+ }
277
+ } catch (err: any) {
278
+ libraryStatus.textContent = `Error loading library`;
279
+ }
280
+ // Clear the selector after use
281
+ libraryInput.value = null as any;
282
+ setTimeout(() => { libraryStatus.textContent = ''; }, 4000);
283
+ });
284
+
285
+ librarySection.appendChild(ui.divV([libraryInput.root, libraryStatus]));
286
+ }
287
+
288
+ // --- Bulk add: from monomer collection ---
289
+ const collectionSection = ui.div([], {style: {marginTop: '4px'}});
290
+ if (libHelper) {
291
+ const collectionInput = ui.input.choice('Add from collection', {items: [] as string[], nullable: true}) as DG.ChoiceInput<string>;
292
+ const collectionStatus = ui.divText('', {style: {fontSize: '11px', color: 'var(--green-2)', marginLeft: '8px', minHeight: '16px'}});
293
+
294
+ // Load collection names asynchronously
295
+ libHelper.listMonomerCollections().then((collectionNames) => {
296
+ collectionInput.items = collectionNames;
297
+ });
298
+
299
+ collectionInput.onChanged.subscribe(async () => {
300
+ const collectionName = collectionInput.value;
301
+ if (!collectionName)
302
+ return;
303
+
304
+ collectionStatus.textContent = 'Loading...';
305
+ try {
306
+ const collection = await libHelper.readMonomerCollection(collectionName);
307
+ const symbols = collection.monomerSymbols ?? [];
308
+ const before = selectedMonomers.length;
309
+ addMonomers(symbols);
310
+ const added = selectedMonomers.length - before;
311
+ const displayName = collectionName.replace(/\.json$/, '');
312
+ collectionStatus.textContent = `Added ${added} monomer(s) from "${displayName}"`;
313
+ } catch (err: any) {
314
+ collectionStatus.textContent = `Error loading collection`;
315
+ }
316
+ // Clear the selector after use
317
+ collectionInput.value = null as any;
318
+ setTimeout(() => { collectionStatus.textContent = ''; }, 4000);
319
+ });
320
+
321
+ collectionSection.appendChild(ui.divV([collectionInput.root, collectionStatus]));
322
+ }
323
+
324
+ // --- Clear all button ---
325
+ const clearBtn = ui.button('Clear all', () => {
326
+ selectedMonomers.length = 0;
327
+ renderTags();
328
+ });
329
+ clearBtn.style.fontSize = '11px';
330
+ clearBtn.style.marginTop = '4px';
331
+
188
332
  renderTags();
189
333
 
190
- const dlg = ui.dialog({title: 'Select Monomers', showFooter: true})
191
- .add(ui.div([input.root, tagsHost], {style: {minWidth: '350px', minHeight: '200px'}}))
334
+ // --- Dialog layout ---
335
+ const contentDiv = ui.div([
336
+ input.root,
337
+ librarySection,
338
+ collectionSection,
339
+ ui.divH([countLabel, clearBtn], {style: {justifyContent: 'space-between', alignItems: 'center'}}),
340
+ tagsHost,
341
+ ], {style: {minWidth: '400px', minHeight: '250px'}});
342
+
343
+ const _dlg = ui.dialog({title: 'Select Monomers', showFooter: true})
344
+ .add(contentDiv)
192
345
  .onOK(() => { resolve(selectedMonomers); })
193
346
  .onCancel(() => { resolve(null); })
194
347
  .show({resizable: true});
195
- // dlg.root.addEventListener('close', () => { hideMenu(); });
196
348
 
197
349
  inputEl.focus();
198
350
  setTimeout(() => { showSuggestions(); }, 0);
@@ -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,8 +6,8 @@ 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
- Library = 'library',
11
11
  }
12
12
 
13
13
  export type PolyToolEnumeratorType = typeof PolyToolEnumeratorTypes[keyof typeof PolyToolEnumeratorTypes];
@@ -27,6 +27,7 @@ export type PolyToolEnumeratorParams = {
27
27
  breadthPlaceholders?: PolyToolBreadthPlaceholder[];
28
28
  keepOriginal?: boolean;
29
29
  trivialName?: boolean;
30
+ fromHelmNotation?: {convert:(helm: string) => string; notationName: string};
30
31
  }
31
32
 
32
33
  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: