@datagrok/helm 3.0.3 → 3.0.5

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,19 +1,28 @@
1
1
  {
2
2
  "name": "@datagrok/helm",
3
3
  "friendlyName": "Helm",
4
- "version": "3.0.3",
4
+ "version": "3.0.5",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
8
8
  },
9
9
  "description": "Provides support for HELM notation (importing, detecting, rendering, conversion).",
10
+ "properties": [
11
+ {
12
+ "name": "MonomersPerRow",
13
+ "description": "Number of monomers per row before a HELM structure wraps to the next line. Counts nucleotides for RNA/DNA.",
14
+ "propertyType": "int",
15
+ "defaultValue": "20",
16
+ "nullable": false
17
+ }
18
+ ],
10
19
  "sources": [
11
20
  "css/helm.css"
12
21
  ],
13
22
  "dependencies": {
14
23
  "@datagrok-libraries/bio": "^6.0.2",
15
24
  "@datagrok-libraries/chem-meta": "^1.2.9",
16
- "@datagrok-libraries/hwe": "^1.0.2",
25
+ "@datagrok-libraries/hwe": "^1.0.5",
17
26
  "@datagrok-libraries/test": "^1.3.1",
18
27
  "@datagrok-libraries/utils": "^4.6.9",
19
28
  "cash-dom": "^8.1.1",
package/src/constants.ts CHANGED
@@ -24,3 +24,15 @@ export const SDF_MONOMER_NAME = 'MonomerName';
24
24
  export const enum TAGS {
25
25
  cellRendererRenderError = '.cell-renderer.render.error',
26
26
  }
27
+
28
+ /** Names of package properties declared in the `properties` section of `package.json`. */
29
+ export const enum HelmPackagePropertiesNames {
30
+ MonomersPerRow = 'MonomersPerRow',
31
+ }
32
+
33
+ /**
34
+ * Fallback for `MonomersPerRow` when the package property is missing or
35
+ * unparseable (a package loaded before its settings, an old install). Kept in
36
+ * step with the `defaultValue` in package.json and with hwe's own default.
37
+ */
38
+ export const DEFAULT_MONOMERS_PER_ROW = 20;
@@ -23,6 +23,14 @@ export namespace funcs {
23
23
  return await grok.functions.call('Helm:EditMoleculeCell', { cell });
24
24
  }
25
25
 
26
+ /**
27
+ * @param {DG.Column} col
28
+ * semType: Macromolecule
29
+ */
30
+ export async function helmPanel(col: DG.Column ): Promise<any> {
31
+ return await grok.functions.call('Helm:HelmPanel', { col });
32
+ }
33
+
26
34
  /**
27
35
  * Adds editor
28
36
  * @param {any} mol
@@ -16,6 +16,7 @@ import './tests/helm-helper-tests';
16
16
  import './tests/helm-substructure-filter';
17
17
  import './tests/helm-activity-cliffs';
18
18
  import './tests/to-atomic-level-ui-non-linear';
19
+ import './tests/wrap-width-tests';
19
20
 
20
21
  export const _package = new DG.Package();
21
22
  export {tests};
@@ -12,6 +12,11 @@ import {ISeqHelper} from '@datagrok-libraries/bio/src/utils/seq-helper';
12
12
 
13
13
  import * as DG from 'datagrok-api/dg';
14
14
 
15
+ import {setMonomersPerRow} from '@datagrok-libraries/hwe';
16
+
17
+ import {HelmPackagePropertiesNames} from './constants';
18
+ import {resolveMonomersPerRow} from './utils/wrap-width';
19
+
15
20
  import {_package} from './package';
16
21
 
17
22
  // hwe migration (Phase 7): the legacy Dojo / JSDraw2 / HELMWebEditor loader and
@@ -52,6 +57,38 @@ export class HelmPackage extends DG.Package {
52
57
 
53
58
  private _initialized: boolean = false;
54
59
 
60
+ // -- Wrapping (line-break) width --
61
+
62
+ /**
63
+ * The package-wide default number of monomers per row, from the
64
+ * `MonomersPerRow` package property (see the `properties` section of
65
+ * package.json). This is the DEFAULT: the column widget in `helmPanel` can
66
+ * override it for the session without touching this value.
67
+ *
68
+ * Falls back to {@link DEFAULT_MONOMERS_PER_ROW} when settings are not
69
+ * loaded yet or hold something unparseable.
70
+ */
71
+ public get defaultMonomersPerRow(): number {
72
+ let raw: unknown;
73
+ try {
74
+ raw = this.settings?.[HelmPackagePropertiesNames.MonomersPerRow];
75
+ } catch {
76
+ // `settings` reaches into the platform; a package instance that is not
77
+ // registered yet (or the standalone test bundle's copy) has nothing to
78
+ // reach into. Fall back rather than break every render.
79
+ raw = undefined;
80
+ }
81
+ return resolveMonomersPerRow(raw);
82
+ }
83
+
84
+ /**
85
+ * Push {@link defaultMonomersPerRow} into hwe's process-wide layout setting,
86
+ * which every renderer / editor / adapter reads at layout time.
87
+ */
88
+ public applyDefaultMonomersPerRow(): void {
89
+ setMonomersPerRow(this.defaultMonomersPerRow);
90
+ }
91
+
55
92
  /** Requires Bio initialized (monomer library). */
56
93
  completeInit(helmHelper: IHelmHelper, libHelper: IMonomerLibHelper): void {
57
94
  this._helmHelper = helmHelper;
package/src/package.g.ts CHANGED
@@ -25,6 +25,15 @@ export function editMoleculeCell(cell: any) : void {
25
25
  PackageFunctions.editMoleculeCell(cell);
26
26
  }
27
27
 
28
+ //name: HELM Renderer
29
+ //tags: widgets, panel
30
+ //input: column col { semType: Macromolecule; units: helm }
31
+ //output: widget result
32
+ //meta.role: widgets,panel
33
+ export async function helmPanel(col: DG.Column) : Promise<any> {
34
+ return await PackageFunctions.helmPanel(col);
35
+ }
36
+
28
37
  //name: Edit Helm...
29
38
  //description: Adds editor
30
39
  //input: semantic_value mol { semType: Macromolecule }
package/src/package.ts CHANGED
@@ -15,6 +15,7 @@ import {HelmInputBase, IHelmHelper, IHelmInputInitOptions} from '@datagrok-libra
15
15
  import {getMonomerLibHelper} from '@datagrok-libraries/bio/src/types/monomer-library';
16
16
 
17
17
  import {getPropertiesWidget} from './widgets/properties-widget';
18
+ import {getWrapWidthWidget} from './widgets/wrap-width-widget';
18
19
  import {HelmGridCellRenderer, HelmGridCellRendererBack} from './utils/helm-grid-cell-renderer';
19
20
  import {HelmPackage} from './package-utils';
20
21
  import {HelmHelper} from './helm-helper';
@@ -70,6 +71,11 @@ async function initHelmInt(): Promise<void> {
70
71
  const libHelper = await getMonomerLibHelper();
71
72
  const helmHelper: IHelmHelper = new HelmHelper(seqHelper, _package.logger, rdKitModule);
72
73
  _package.completeInit(helmHelper, libHelper);
74
+ // Seed hwe's process-wide row-wrap width from the `MonomersPerRow` package
75
+ // property. Must run before the first cell renders; the column widget in
76
+ // `helmPanel` can then override it for the session without changing this
77
+ // default.
78
+ _package.applyDefaultMonomersPerRow();
73
79
  } catch (err: any) {
74
80
  const [errMsg, errStack] = errInfo(err);
75
81
  grok.shell.error(`Package \'Helm\' init error:\n${errMsg}`);
@@ -151,6 +157,20 @@ export class PackageFunctions {
151
157
  checkMonomersAndOpenWebEditor(cell, undefined, undefined);
152
158
  }
153
159
 
160
+ @grok.decorators.panel({
161
+ name: 'HELM Renderer',
162
+ meta: {role: 'widgets'},
163
+ tags: ['widgets', 'panel'],
164
+ })
165
+ static async helmPanel(
166
+ @grok.decorators.param({'options': {'semType': 'Macromolecule', 'units': 'helm'}}) col: DG.Column): Promise<DG.Widget> {
167
+ if (col.meta.units !== NOTATION.HELM)
168
+ return new DG.Widget(ui.divText('These settings apply only to HELM columns'));
169
+ // Session-scoped row-wrap width. The package-wide default lives in the
170
+ // `MonomersPerRow` package property and is not touched here.
171
+ return getWrapWidthWidget();
172
+ }
173
+
154
174
 
155
175
  @grok.decorators.func({
156
176
  'meta': {'action': 'Edit Helm...'},
@@ -0,0 +1,132 @@
1
+ import * as DG from 'datagrok-api/dg';
2
+
3
+ import {after, before, category, expect, test} from '@datagrok-libraries/test/src/test';
4
+ import {getWrapSettings, setMonomersPerRow} from '@datagrok-libraries/hwe';
5
+
6
+ import {initHelmMainPackage} from './utils';
7
+ import {getWrapWidthWidget} from '../widgets/wrap-width-widget';
8
+ import {resolveMonomersPerRow} from '../utils/wrap-width';
9
+ import {DEFAULT_MONOMERS_PER_ROW} from '../constants';
10
+
11
+ // The int input inside the widget produced by `getWrapWidthWidget`.
12
+ function widgetInput(widget: DG.Widget): HTMLInputElement {
13
+ const input = widget.root.querySelector('input') as HTMLInputElement | null;
14
+ if (input === null) throw new Error('wrap-width widget: no input element');
15
+ return input;
16
+ }
17
+
18
+ // Set the input the way a user would, so the widget's change handler runs.
19
+ function typeValue(input: HTMLInputElement, value: number): void {
20
+ input.value = String(value);
21
+ input.dispatchEvent(new Event('input', {bubbles: true}));
22
+ input.dispatchEvent(new Event('change', {bubbles: true}));
23
+ }
24
+
25
+ category('WrapWidth: package property', () => {
26
+ test('reads an int property', async () => {
27
+ expect(resolveMonomersPerRow(30), 30);
28
+ });
29
+
30
+ test('reads a string defaultValue straight from package.json', async () => {
31
+ // An `int` property that has not round-tripped through the server arrives
32
+ // as the string written in package.json.
33
+ expect(resolveMonomersPerRow('30'), 30);
34
+ expect(resolveMonomersPerRow('20'), 20);
35
+ });
36
+
37
+ test('falls back when settings are missing or unusable', async () => {
38
+ // A NaN reaching the layout would produce a drawing with no coordinates,
39
+ // so every unusable value must land on the default instead.
40
+ for (const raw of [undefined, null, '', 'abc', Number.NaN, 0, -5, {}])
41
+ expect(resolveMonomersPerRow(raw), DEFAULT_MONOMERS_PER_ROW);
42
+ });
43
+
44
+ test('default matches the value declared in package.json', async () => {
45
+ expect(DEFAULT_MONOMERS_PER_ROW, 20);
46
+ });
47
+ });
48
+
49
+ category('WrapWidth: session widget', () => {
50
+ // The wrap width is process-wide state inside hwe; put it back afterwards so
51
+ // a failure here cannot change how the rest of the suite renders.
52
+ let restore: number;
53
+
54
+ before(async () => {
55
+ await initHelmMainPackage();
56
+ restore = getWrapSettings().monomersPerRow;
57
+ });
58
+
59
+ after(async () => {
60
+ setMonomersPerRow(restore);
61
+ });
62
+
63
+ test('opens showing the current session value', async () => {
64
+ setMonomersPerRow(14);
65
+ const widget = getWrapWidthWidget();
66
+ try {
67
+ expect(Number(widgetInput(widget).value), 14);
68
+ } finally {
69
+ widget.detach();
70
+ }
71
+ });
72
+
73
+ test('editing the input changes the session value', async () => {
74
+ setMonomersPerRow(DEFAULT_MONOMERS_PER_ROW);
75
+ const widget = getWrapWidthWidget();
76
+ try {
77
+ typeValue(widgetInput(widget), 8);
78
+ expect(getWrapSettings().monomersPerRow, 8);
79
+ } finally {
80
+ widget.detach();
81
+ }
82
+ });
83
+
84
+ test('editing does NOT change the package default', async () => {
85
+ // This is the whole point of the split: the package property stays the
86
+ // value every new session starts from.
87
+ const widget = getWrapWidthWidget();
88
+ try {
89
+ typeValue(widgetInput(widget), 5);
90
+ expect(getWrapSettings().monomersPerRow, 5);
91
+ expect(resolveMonomersPerRow('20'), 20);
92
+ expect(DEFAULT_MONOMERS_PER_ROW, 20);
93
+ } finally {
94
+ widget.detach();
95
+ }
96
+ });
97
+
98
+ test('reflects a change made elsewhere', async () => {
99
+ const widget = getWrapWidthWidget();
100
+ try {
101
+ setMonomersPerRow(13);
102
+ expect(Number(widgetInput(widget).value), 13);
103
+ } finally {
104
+ widget.detach();
105
+ }
106
+ });
107
+
108
+ test('stops tracking after detach', async () => {
109
+ const widget = getWrapWidthWidget();
110
+ const input = widgetInput(widget);
111
+ typeValue(input, 9);
112
+ widget.detach();
113
+ setMonomersPerRow(17);
114
+ // The subscription goes through `widget.sub`, so `detach` unsubscribes it.
115
+ expect(Number(input.value), 9);
116
+ expect(getWrapSettings().monomersPerRow, 17);
117
+ });
118
+
119
+ test('a rejected value leaves the session value alone', async () => {
120
+ setMonomersPerRow(11);
121
+ const widget = getWrapWidthWidget();
122
+ try {
123
+ const input = widgetInput(widget);
124
+ input.value = '';
125
+ input.dispatchEvent(new Event('input', {bubbles: true}));
126
+ input.dispatchEvent(new Event('change', {bubbles: true}));
127
+ expect(getWrapSettings().monomersPerRow, 11);
128
+ } finally {
129
+ widget.detach();
130
+ }
131
+ });
132
+ });
@@ -0,0 +1,18 @@
1
+ import {DEFAULT_MONOMERS_PER_ROW} from '../constants';
2
+
3
+ /**
4
+ * Read the `MonomersPerRow` package property into a usable row width.
5
+ *
6
+ * Datagrok hands an `int` property back as a number once it has round-tripped
7
+ * through the server, but a `defaultValue` straight out of package.json arrives
8
+ * as the string written there, and a package whose settings have not loaded yet
9
+ * gives `undefined`. All three have to end up at a sane integer — a `NaN` here
10
+ * would reach the layout and produce a drawing with no coordinates.
11
+ *
12
+ * @param {unknown} raw The raw property value.
13
+ * @return {number} A row width of at least 1, or {@link DEFAULT_MONOMERS_PER_ROW}.
14
+ */
15
+ export function resolveMonomersPerRow(raw: unknown): number {
16
+ const parsed = typeof raw === 'number' ? raw : Number.parseInt(String(raw ?? ''), 10);
17
+ return Number.isFinite(parsed) && parsed >= 1 ? Math.round(parsed) : DEFAULT_MONOMERS_PER_ROW;
18
+ }
@@ -0,0 +1,73 @@
1
+ import * as grok from 'datagrok-api/grok';
2
+ import * as ui from 'datagrok-api/ui';
3
+ import * as DG from 'datagrok-api/dg';
4
+
5
+ import {Subscription} from 'rxjs';
6
+
7
+ // The wrapping width is a process-wide setting inside hwe, read at layout time
8
+ // by every HelmService / editor / adapter (see @datagrok-libraries/hwe API.md
9
+ // §4.3). That is what lets one input here change both the grid cells and the
10
+ // editor without hunting down the several service instances the package owns.
11
+ import {getWrapSettings, setMonomersPerRow, onWrapSettingsChanged} from '@datagrok-libraries/hwe';
12
+
13
+ import {_package} from '../package';
14
+
15
+ /** Widest row the input allows. Well past any readable structure. */
16
+ const MAX_MONOMERS_PER_ROW = 100;
17
+
18
+ /** Repaint every open grid so HELM cells re-render at the new width. */
19
+ function invalidateHelmGrids(): void {
20
+ // hwe's own layout / render caches key on a setting version and have already
21
+ // missed by the time we get here, so a plain repaint is enough.
22
+ for (const view of grok.shell.tableViews) {
23
+ try {
24
+ view.grid?.invalidate();
25
+ } catch (_err) {
26
+ // A view can be closing while we iterate; a failed repaint is not worth
27
+ // failing the whole widget over.
28
+ }
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Session-scoped control over where HELM structures break into a new line.
34
+ *
35
+ * Deliberately does NOT write the package property: the `MonomersPerRow`
36
+ * package setting stays the default that every session starts from, and this
37
+ * widget adjusts the current session only. "Reset" puts the session back onto
38
+ * that default rather than clearing it.
39
+ *
40
+ * @return {DG.Widget} The row-width editor.
41
+ */
42
+ export function getWrapWidthWidget(): DG.Widget {
43
+ const input = ui.input.int('Monomers per row', {
44
+ value: getWrapSettings().monomersPerRow,
45
+ min: 1,
46
+ max: MAX_MONOMERS_PER_ROW,
47
+ tooltipText: 'Number of monomers drawn per row before the structure wraps. ' +
48
+ 'Counts nucleotides for RNA/DNA. Applies to this session only. The default comes from the Helm package property',
49
+ onValueChanged: (value: number | null) => {
50
+ if (value === null || !Number.isFinite(value)) return;
51
+ setMonomersPerRow(value);
52
+ invalidateHelmGrids();
53
+ },
54
+ });
55
+
56
+ const packageDefault = _package.defaultMonomersPerRow;
57
+ const resetLink = ui.link('Reset to package default', () => {
58
+ setMonomersPerRow(packageDefault);
59
+ invalidateHelmGrids();
60
+ }, `Back to ${packageDefault} — the MonomersPerRow package property`, {style: {marginTop: '4px'}});
61
+
62
+ const widget = new DG.Widget(ui.divV([input.root, resetLink]));
63
+
64
+ // Keep the input honest if the setting is changed from anywhere else (the
65
+ // editor, another widget, a script). `setMonomersPerRow` clamps and rounds,
66
+ // so this is also how the input reflects a value it did not produce itself.
67
+ const off = onWrapSettingsChanged((settings) => {
68
+ if (input.value !== settings.monomersPerRow) input.value = settings.monomersPerRow;
69
+ });
70
+ widget.sub(new Subscription(off));
71
+
72
+ return widget;
73
+ }