@datagrok/sequence-translator 1.10.6 → 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.
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.6",
4
+ "version": "1.10.7",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
@@ -22,7 +22,7 @@
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",
@@ -55,7 +55,6 @@
55
55
  "@typescript-eslint/eslint-plugin": "^7.2.0",
56
56
  "@typescript-eslint/parser": "^7.2.0",
57
57
  "css-loader": "^6.7.3",
58
- "datagrok-tools": "^5.0.0",
59
58
  "eslint": "^8.57.0",
60
59
  "eslint-config-google": "^0.14.0",
61
60
  "style-loader": "^3.3.1",
package/src/package.g.ts CHANGED
@@ -221,3 +221,9 @@ export async function getPolyToolCombineDialog() : Promise<void> {
221
221
  export function applyNotationProviderForCyclized(col: DG.Column<any>, separator: string) : void {
222
222
  PackageFunctions.applyNotationProviderForCyclized(col, separator);
223
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';
@@ -400,6 +400,14 @@ export class PackageFunctions {
400
400
  col.tags[PolyToolTags.dataRole] = 'template';
401
401
  col.temp[SeqTemps.notationProvider] = new CyclizedNotationProvider(separator, _package.helmHelper);
402
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
+ }
403
411
  }
404
412
 
405
413
  //name: getSpecifiedAppView
@@ -25,6 +25,7 @@ import {PolyToolEnumeratorParams, PolyToolEnumeratorType, PolyToolEnumeratorType
25
25
  import {getLibrariesList, LIB_PATH} from './utils';
26
26
  import {doPolyToolEnumerateHelm, PT_HELM_EXAMPLE} from './pt-enumeration-helm';
27
27
  import {PolyToolPlaceholdersInput} from './pt-placeholders-input';
28
+ import {showMonomerSelectionDialog} from './pt-monomer-selection-dialog';
28
29
  import {defaultErrorHandler} from '../utils/err-info';
29
30
  import {PolyToolPlaceholdersBreadthInput} from './pt-placeholders-breadth-input';
30
31
  import {PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
@@ -294,6 +295,30 @@ async function getPolyToolEnumerateDialog(
294
295
  inputs.library.root.style.setProperty('display', 'none');
295
296
  inputs.trivialNameCol.addOptions(trivialNameSampleDiv);
296
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
+
297
322
  let placeholdersValidity: string | null = null;
298
323
  inputs.placeholders.addValidator((value: string): string | null => {
299
324
  const errors: string[] = [];
@@ -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
+ }
@@ -7,6 +7,15 @@ import {PolyToolBreadthPlaceholder} from './types';
7
7
  import {parseMonomerSymbolList} from './pt-placeholders-input';
8
8
  import {GridCellRenderArgs} from 'datagrok-api/dg';
9
9
 
10
+ /** Callback invoked when user double-clicks a Monomers cell in the breadth grid.
11
+ * @param start 0-based start position index
12
+ * @param end 0-based end position index
13
+ * @param currentMonomers current monomer symbols
14
+ * @returns new monomer symbols, or null if cancelled */
15
+ export type BreadthMonomerCellEditCallback = (
16
+ start: number, end: number, currentMonomers: string[],
17
+ ) => Promise<string[] | null>;
18
+
10
19
  export class PolyToolPlaceholdersBreadthInput extends DG.JsInputBase<DG.DataFrame> {
11
20
  get inputType(): string { return 'Breadth'; }
12
21
 
@@ -30,6 +39,9 @@ export class PolyToolPlaceholdersBreadthInput extends DG.JsInputBase<DG.DataFram
30
39
  return dfToPlaceholdersBreadth(this.grid.dataFrame);
31
40
  }
32
41
 
42
+ /** Set this callback to handle double-click editing of the Monomers column */
43
+ onMonomerCellEdit: BreadthMonomerCellEditCallback | null = null;
44
+
33
45
  private readonly gridHost: HTMLDivElement;
34
46
  public grid: DG.Grid;
35
47
 
@@ -91,6 +103,18 @@ export class PolyToolPlaceholdersBreadthInput extends DG.JsInputBase<DG.DataFram
91
103
  }
92
104
  });
93
105
 
106
+ // Make Monomers column non-editable (editing via double-click dialog)
107
+ const monomersGridCol = this.grid.columns.byName('Monomers');
108
+ if (monomersGridCol)
109
+ monomersGridCol.editable = false;
110
+
111
+ // Double-click on Monomers cell opens the monomer selection dialog
112
+ this.subs.push(this.grid.onCellDoubleClick.subscribe((gc: DG.GridCell) => {
113
+ if (gc.tableColumn?.name === 'Monomers' &&
114
+ gc.tableRowIndex != null && gc.tableRowIndex >= 0)
115
+ this.handleMonomerCellDoubleClick(gc.tableRowIndex);
116
+ }));
117
+
94
118
  this.updateGridHeight(heightRowCount ?? this.grid.dataFrame.rowCount + 0.7);
95
119
  this.subs.push(ui.onSizeChanged(this.grid.root)
96
120
  .subscribe(this.gridRootOnSizeChanged.bind(this)));
@@ -140,6 +164,21 @@ export class PolyToolPlaceholdersBreadthInput extends DG.JsInputBase<DG.DataFram
140
164
 
141
165
  // -- Handle events --
142
166
 
167
+ private async handleMonomerCellDoubleClick(tableRowIdx: number): Promise<void> {
168
+ if (!this.onMonomerCellEdit)
169
+ return;
170
+ const df = this.grid.dataFrame;
171
+ const start = parseInt(df.get('Start', tableRowIdx)) - 1;
172
+ const end = parseInt(df.get('End', tableRowIdx)) - 1;
173
+ if (isNaN(start) || isNaN(end))
174
+ return;
175
+ const currentStr: string = df.get('Monomers', tableRowIdx) ?? '';
176
+ const currentMonomers = parseMonomerSymbolList(currentStr);
177
+ const result = await this.onMonomerCellEdit(start, end, currentMonomers);
178
+ if (result !== null)
179
+ df.set('Monomers', tableRowIdx, result.join(', '));
180
+ }
181
+
143
182
  private gridRootOnSizeChanged(): void {
144
183
  this.grid.columns.byIndex(4)!.width = this.grid.root.clientWidth - this.grid.horzScroll.root.offsetWidth -
145
184
  this.grid.columns.byIndex(0)!.width - this.grid.columns.byIndex(1)!.width -
@@ -7,6 +7,12 @@ import {fromEvent, Unsubscribable} from 'rxjs';
7
7
  import {PolyToolPlaceholder} from './types';
8
8
  import {GridCellRenderArgs} from 'datagrok-api/dg';
9
9
 
10
+ /** Callback invoked when user double-clicks a Monomers cell.
11
+ * @param position 0-based position index
12
+ * @param currentMonomers current monomer symbols
13
+ * @returns new monomer symbols, or null if cancelled */
14
+ export type MonomerCellEditCallback = (position: number, currentMonomers: string[]) => Promise<string[] | null>;
15
+
10
16
  export class PolyToolPlaceholdersInput extends DG.JsInputBase<DG.DataFrame> {
11
17
  get inputType(): string { return 'Positions'; }
12
18
 
@@ -38,6 +44,9 @@ export class PolyToolPlaceholdersInput extends DG.JsInputBase<DG.DataFrame> {
38
44
  return dfToPlaceholders(this.grid.dataFrame);
39
45
  }
40
46
 
47
+ /** Set this callback to handle double-click editing of the Monomers column */
48
+ onMonomerCellEdit: MonomerCellEditCallback | null = null;
49
+
41
50
  private readonly gridHost: HTMLDivElement;
42
51
  private grid!: DG.Grid;
43
52
 
@@ -89,6 +98,17 @@ export class PolyToolPlaceholdersInput extends DG.JsInputBase<DG.DataFrame> {
89
98
  }
90
99
  });
91
100
 
101
+ // Make Monomers column non-editable (editing is done via double-click dialog)
102
+ const monomersGridCol = this.grid.columns.byName('Monomers');
103
+ if (monomersGridCol)
104
+ monomersGridCol.editable = false;
105
+
106
+ // Double-click on Monomers cell opens the monomer selection dialog
107
+ this.subs.push(this.grid.onCellDoubleClick.subscribe((gc: DG.GridCell) => {
108
+ if (gc.tableColumn?.name === 'Monomers' && gc.tableRowIndex != null && gc.tableRowIndex >= 0)
109
+ this.handleMonomerCellDoubleClick(gc.tableRowIndex);
110
+ }));
111
+
92
112
  this.updateGridHeight(heightRowCount ?? this.grid.dataFrame.rowCount + 0.7);
93
113
  this.subs.push(ui.onSizeChanged(this.grid.root)
94
114
  .subscribe(this.gridRootOnSizeChanged.bind(this)));
@@ -160,6 +180,20 @@ export class PolyToolPlaceholdersInput extends DG.JsInputBase<DG.DataFrame> {
160
180
 
161
181
  // -- Handle events --
162
182
 
183
+ private async handleMonomerCellDoubleClick(tableRowIdx: number): Promise<void> {
184
+ if (!this.onMonomerCellEdit)
185
+ return;
186
+ const df = this.grid.dataFrame;
187
+ const position = parseInt(df.get('Position', tableRowIdx)) - 1;
188
+ if (isNaN(position))
189
+ return;
190
+ const currentMonomersStr: string = df.get('Monomers', tableRowIdx) ?? '';
191
+ const currentMonomers = parseMonomerSymbolList(currentMonomersStr);
192
+ const result = await this.onMonomerCellEdit(position, currentMonomers);
193
+ if (result !== null)
194
+ df.set('Monomers', tableRowIdx, result.join(', '));
195
+ }
196
+
163
197
  private gridRootOnSizeChanged(): void {
164
198
  this.grid.columns.byIndex(3)!.width = this.grid.root.clientWidth - this.grid.horzScroll.root.offsetWidth -
165
199
  this.grid.columns.byIndex(0)!.width - this.grid.columns.byIndex(1)!.width -
@@ -6,7 +6,7 @@ import wu from 'wu';
6
6
 
7
7
  /* eslint-disable max-len */
8
8
  import {ISeqHelper} from '@datagrok-libraries/bio/src/utils/seq-helper';
9
- import {INotationProvider, ISeqSplitted, SeqSplittedBase, SplitterFunc} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
9
+ import {INotationProvider, ISeqSplitted, NotationProviderBase, SeqSplittedBase, SplitterFunc} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
10
10
  import {getSplitterWithSeparator} from '@datagrok-libraries/bio/src/utils/macromolecule';
11
11
  import {GAP_SYMBOL, GapOriginals, NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
12
12
  import {CellRendererBackBase} from '@datagrok-libraries/bio/src/utils/cell-renderer-back-base';
@@ -22,16 +22,22 @@ import {CyclizedCellRendererBack} from './cell-renderer-cyclized';
22
22
 
23
23
  /* eslint-enable max-len */
24
24
 
25
- export class CyclizedNotationProvider implements INotationProvider {
25
+ export class CyclizedNotationProvider extends NotationProviderBase implements INotationProvider {
26
26
  private readonly separatorSplitter: SplitterFunc;
27
27
  public readonly splitter: SplitterFunc;
28
28
 
29
+ static override get notationName(): string { return 'Harmonized Sequence'; }
30
+ static override get implementsFromHelm(): boolean { return false; }
31
+ static override convertFromHelm(helm: string, options: any): string {
32
+ throw new Error('converting from helm not supported yet');
33
+ }
29
34
  get defaultGapOriginal(): string { return ''; }
30
35
 
31
36
  constructor(
32
37
  public readonly separator: string,
33
38
  protected readonly helmHelper: IHelmHelper
34
39
  ) {
40
+ super();
35
41
  this.separatorSplitter = getSplitterWithSeparator(this.separator);
36
42
  this.splitter = this._splitter.bind(this);
37
43
  }