@datagrok/bio 2.11.28 → 2.11.30

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 (58) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/23.js +2 -0
  3. package/dist/23.js.map +1 -0
  4. package/dist/282.js +2 -0
  5. package/dist/282.js.map +1 -0
  6. package/dist/36.js +2 -0
  7. package/dist/36.js.map +1 -0
  8. package/dist/40.js +2 -0
  9. package/dist/40.js.map +1 -0
  10. package/dist/413.js +2 -0
  11. package/dist/413.js.map +1 -0
  12. package/dist/42.js +1 -1
  13. package/dist/42.js.map +1 -1
  14. package/dist/427.js +2 -0
  15. package/dist/427.js.map +1 -0
  16. package/dist/65.js +2 -0
  17. package/dist/65.js.map +1 -0
  18. package/dist/709.js +3 -0
  19. package/dist/709.js.map +1 -0
  20. package/dist/package-test.js +1 -1
  21. package/dist/package-test.js.map +1 -1
  22. package/dist/package.js +1 -1
  23. package/dist/package.js.map +1 -1
  24. package/files/tests/to-atomic-level-msa-fasta-input.csv +10 -0
  25. package/files/tests/to-atomic-level-msa-fasta-output.csv +2178 -0
  26. package/package.json +9 -9
  27. package/src/package.ts +12 -10
  28. package/src/tests/msa-tests.ts +2 -2
  29. package/src/tests/pepsea-tests.ts +1 -1
  30. package/src/tests/renderers-test.ts +16 -16
  31. package/src/tests/splitters-test.ts +7 -12
  32. package/src/tests/substructure-filters-tests.ts +277 -55
  33. package/src/tests/to-atomic-level-tests.ts +32 -22
  34. package/src/tests/utils/sequences-generators.ts +6 -3
  35. package/src/tests/utils.ts +6 -6
  36. package/src/utils/cell-renderer.ts +2 -0
  37. package/src/utils/docker.ts +36 -0
  38. package/src/viewers/vd-regions-viewer.ts +4 -0
  39. package/src/viewers/web-logo-viewer.ts +4 -0
  40. package/src/widgets/bio-substructure-filter-helm.ts +168 -0
  41. package/src/widgets/bio-substructure-filter-types.ts +131 -0
  42. package/src/widgets/bio-substructure-filter.ts +215 -175
  43. package/src/widgets/composition-analysis-widget.ts +1 -1
  44. package/dist/100.js +0 -2
  45. package/dist/100.js.map +0 -1
  46. package/dist/118.js +0 -2
  47. package/dist/118.js.map +0 -1
  48. package/dist/361.js +0 -2
  49. package/dist/361.js.map +0 -1
  50. package/dist/471.js +0 -2
  51. package/dist/471.js.map +0 -1
  52. package/dist/649.js +0 -2
  53. package/dist/649.js.map +0 -1
  54. package/dist/664.js +0 -2
  55. package/dist/664.js.map +0 -1
  56. package/dist/886.js +0 -3
  57. package/dist/886.js.map +0 -1
  58. /package/dist/{886.js.LICENSE.txt → 709.js.LICENSE.txt} +0 -0
@@ -21,22 +21,30 @@ import {_package} from '../package-test';
21
21
  const appPath = 'System:AppData/Bio';
22
22
  const fileSource = new DG.FileSource(appPath);
23
23
 
24
- const testNames: { [k: string]: string } = {
25
- PT: 'peptides-fasta',
26
- DNA: 'dna-fasta',
27
- MSA: 'msa-separator',
28
- };
29
-
30
- const inputPath: { [k: string]: string } = {
31
- PT: 'tests/to-atomic-level-peptides-fasta-input.csv',
32
- DNA: 'tests/to-atomic-level-dna-fasta-input.csv',
33
- MSA: 'tests/to-atomic-level-msa-separator-input.csv',
34
- };
24
+ const enum Tests {
25
+ PT = 'peptides-fasta',
26
+ DNA = 'dna-fasta',
27
+ MSA_SEPARATOR = 'msa-separator',
28
+ MSA_FASTA = 'msa-fasta',
29
+ }
35
30
 
36
- const outputPath: { [k: string]: string } = {
37
- PT: 'tests/to-atomic-level-peptides-fasta-output.csv',
38
- DNA: 'tests/to-atomic-level-dna-fasta-output.csv',
39
- MSA: 'tests/to-atomic-level-msa-separator-output.csv',
31
+ const TestsData: { [testName: string]: { inPath: string, outPath: string } } = {
32
+ [Tests.PT]: {
33
+ inPath: 'tests/to-atomic-level-peptides-fasta-input.csv',
34
+ outPath: 'tests/to-atomic-level-peptides-fasta-output.csv'
35
+ },
36
+ [Tests.DNA]: {
37
+ inPath: 'tests/to-atomic-level-dna-fasta-input.csv',
38
+ outPath: 'tests/to-atomic-level-dna-fasta-output.csv'
39
+ },
40
+ [Tests.MSA_SEPARATOR]: {
41
+ inPath: 'tests/to-atomic-level-msa-separator-input.csv',
42
+ outPath: 'tests/to-atomic-level-msa-separator-output.csv'
43
+ },
44
+ [Tests.MSA_FASTA]: {
45
+ inPath: 'tests/to-atomic-level-msa-fasta-input.csv',
46
+ outPath: 'tests/to-atomic-level-msa-fasta-output.csv'
47
+ },
40
48
  };
41
49
 
42
50
  const inputColName = 'sequence';
@@ -57,10 +65,12 @@ category('toAtomicLevel', async () => {
57
65
  await setUserLibSettingsForTests();
58
66
  await monomerLibHelper.loadLibraries(true);
59
67
 
60
- for (const key in testNames) {
61
- sourceDf[key] = DG.DataFrame.fromCsv((await fileSource.readAsText(inputPath[key])).replace(/\n$/, ''));
62
- await grok.data.detectSemanticTypes(sourceDf[key]);
63
- targetDf[key] = DG.DataFrame.fromCsv((await fileSource.readAsText(outputPath[key])).replace(/\n$/, ''));
68
+ for (const [testName, testData] of Object.entries(TestsData)) {
69
+ const inputPath = testData.inPath;
70
+
71
+ sourceDf[testName] = DG.DataFrame.fromCsv((await fileSource.readAsText(testData.inPath)).replace(/\n$/, ''));
72
+ await grok.data.detectSemanticTypes(sourceDf[testName]);
73
+ targetDf[testName] = DG.DataFrame.fromCsv((await fileSource.readAsText(testData.outPath)).replace(/\n$/, ''));
64
74
  }
65
75
  });
66
76
 
@@ -79,9 +89,9 @@ category('toAtomicLevel', async () => {
79
89
  expectArray(obtainedArray, expectedArray);
80
90
  }
81
91
 
82
- for (const key in testNames) {
83
- test(`${testNames[key]}`, async () => {
84
- await getTestResult(sourceDf[key], targetDf[key]);
92
+ for (const [testName, testData] of Object.entries(TestsData)) {
93
+ test(`${testName}`, async () => {
94
+ await getTestResult(sourceDf[testName], targetDf[testName]);
85
95
  });
86
96
  }
87
97
 
@@ -1,8 +1,11 @@
1
1
  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 {ALIGNMENT, ALPHABET, NOTATION, TAGS as bioTAGS} from '@datagrok-libraries/bio/src/utils/macromolecule';
5
- import {awaitCheck} from '@datagrok-libraries/utils/src/test';
6
+ import {expect} from '@datagrok-libraries/utils/src/test';
7
+
8
+ import {awaitGrid} from '../utils';
6
9
 
7
10
 
8
11
  export function generateManySequences(): DG.Column[] {
@@ -32,8 +35,8 @@ export async function performanceTest(generateFunc: () => DG.Column[], testName:
32
35
  const col: DG.Column = df.columns.byName('MSA');
33
36
  const view: DG.TableView = grok.shell.addTableView(df);
34
37
 
35
- await awaitCheck(() => { return view.grid.dataFrame !== df; },
36
- 'View grid has wrong data frame ', 100);
38
+ await awaitGrid(view.grid);
39
+ expect(view.grid.dataFrame.id, df.id);
37
40
 
38
41
  const endTime: number = Date.now();
39
42
  const elapsedTime: number = endTime - startTime;
@@ -3,6 +3,8 @@ import * as grok from 'datagrok-api/grok';
3
3
 
4
4
  import {delay, expect, testEvent} from '@datagrok-libraries/utils/src/test';
5
5
 
6
+ import {startDockerContainer} from '../utils/docker';
7
+
6
8
  import {_package} from '../package-test';
7
9
 
8
10
  export async function loadFileAsText(name: string): Promise<string> {
@@ -34,12 +36,10 @@ export function _testTableIsNotEmpty(table: DG.DataFrame): void {
34
36
 
35
37
  /** Waits if container is not started
36
38
  * @param {number} ms - time to wait in milliseconds */
37
- export async function awaitContainerStart(ms: number = 10000): Promise<void> {
38
- const container = await grok.dapi.docker.dockerContainers.filter('bio').first();
39
- if (container.status !== 'started' && container.status !== 'checking')
40
- await delay(ms);
41
- // TODO: Enable with new JS API version
42
- // await grok.dapi.docker.dockerContainers.run(container.id, true);
39
+ export async function awaitContainerStart(ms: number = 30000): Promise<void> {
40
+ const dc = await grok.dapi.docker.dockerContainers.filter('bio').first();
41
+ const dcId = dc.id;
42
+ await startDockerContainer(dcId, ms);
43
43
  }
44
44
 
45
45
  export async function awaitGrid(grid: DG.Grid, timeout: number = 5000): Promise<void> {
@@ -215,6 +215,8 @@ export class MacromoleculeSequenceCellRenderer extends DG.GridCellRenderer {
215
215
 
216
216
  const tempReferenceSequence: string | null = tableColTemp[tempTAGS.referenceSequence];
217
217
  const tempCurrentWord: string | null = tableColTemp[tempTAGS.currentWord];
218
+ if (tempCurrentWord && tableCol?.dataFrame?.currentRowIdx === -1)
219
+ tableColTemp[tempTAGS.currentWord] = null;
218
220
  const referenceSequence: ISeqSplitted = splitterFunc(
219
221
  ((tempReferenceSequence != null) && (tempReferenceSequence != '')) ?
220
222
  tempReferenceSequence : tempCurrentWord ?? '');
@@ -0,0 +1,36 @@
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
+ import {delay} from '@datagrok-libraries/utils/src/test';
5
+
6
+ export async function startDockerContainer(dcId: string, timeout: number = 30000): Promise<void> {
7
+ // TODO: Use the new dockerContainers API
8
+ const res = await grok.dapi.docker.dockerContainers.run(dcId /*, true */);
9
+ let end: boolean = false;
10
+ for (let i = 0; i < timeout / 200; ++i) {
11
+ const dc = await grok.dapi.docker.dockerContainers.find(dcId);
12
+ switch (dc.status) {
13
+ case 'stopped': {
14
+ await grok.dapi.docker.dockerContainers.run(dcId);
15
+ break;
16
+ }
17
+ case 'pending change':
18
+ case 'changing': {
19
+ // skip to wait
20
+ break;
21
+ }
22
+ case 'checking':
23
+ case 'started': {
24
+ end = true;
25
+ break;
26
+ }
27
+ case 'error': {
28
+ throw new Error('Docker container error state.');
29
+ }
30
+ }
31
+ if (end) break;
32
+ await delay(200);
33
+ }
34
+ if (!end) throw new Error('Docker container run timeout.');
35
+ // this.dc = await grok.dapi.docker.dockerContainers.find(dcId);
36
+ }
@@ -520,5 +520,9 @@ export class VdRegionsViewer extends DG.JsViewer implements IVdRegionsViewer {
520
520
  await testEvent(this.onRendered, () => {}, () => {
521
521
  this.invalidate();
522
522
  }, timeout);
523
+
524
+ // Rethrow stored syncer error (for test purposes)
525
+ const viewErrors = this.viewSyncer.resetErrors();
526
+ if (viewErrors.length > 0) throw viewErrors[0];
523
527
  }
524
528
  }
@@ -1297,6 +1297,10 @@ export class WebLogoViewer extends DG.JsViewer implements IWebLogoViewer {
1297
1297
  await testEvent(this.onRendered, () => {}, () => {
1298
1298
  this.invalidate();
1299
1299
  }, timeout);
1300
+
1301
+ // Rethrow stored syncer error (for test purposes)
1302
+ const viewErrors = this.viewSyncer.resetErrors();
1303
+ if (viewErrors.length > 0) throw viewErrors[0];
1300
1304
  }
1301
1305
  }
1302
1306
 
@@ -0,0 +1,168 @@
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 $ from 'cash-dom';
6
+ import {fromEvent, Observable, Subject, Unsubscribable} from 'rxjs';
7
+
8
+ import {IHelmWebEditor, IWebEditorApp} from '@datagrok-libraries/bio/src/helm/types';
9
+ import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
10
+ import {ILogger} from '@datagrok-libraries/bio/src/utils/logger';
11
+ import {errInfo} from '@datagrok-libraries/bio/src/utils/err-info';
12
+ import {delay, testEvent} from '@datagrok-libraries/utils/src/test';
13
+
14
+ import {updateDivInnerHTML} from '../utils/ui-utils';
15
+ import {helmSubstructureSearch} from '../substructure-search/substructure-search';
16
+ import {BioFilterBase, BioFilterProps} from './bio-substructure-filter-types';
17
+
18
+ import {_package} from '../package';
19
+
20
+ export class HelmBioFilter extends BioFilterBase<BioFilterProps> /* implements IRenderer */ {
21
+ readonly emptyProps = new BioFilterProps('');
22
+
23
+ helmEditor: IHelmWebEditor;
24
+ _filterPanel = ui.div('', {style: {cursor: 'pointer'}});
25
+
26
+ private readonly logger: ILogger;
27
+
28
+ private static viewerCounter: number = -1;
29
+ private readonly viewerId: number = ++HelmBioFilter.viewerCounter;
30
+
31
+ private viewerToLog(): string { return `HelmBioFilter<${this.viewerId}>`; }
32
+
33
+ get type(): string { return 'HelmBioFilter'; }
34
+
35
+ constructor() {
36
+ super();
37
+ this.logger = _package.logger;
38
+ }
39
+
40
+ viewSubs: Unsubscribable[] = [];
41
+
42
+ async detach(): Promise<void> {
43
+ await super.detach();
44
+ for (const sub of this.viewSubs) sub.unsubscribe();
45
+ }
46
+
47
+ async attach(): Promise<void> {
48
+ const logPrefix = `${this.viewerToLog()}.init()`;
49
+ try {
50
+ const helmHelper = await getHelmHelper();
51
+ this.helmEditor = helmHelper.createHelmWebEditor();
52
+ this.logger.warning('TEST: HelmBioFilter.init().sync() waitForElementInDom waiting...');
53
+ await ui.tools.waitForElementInDom(this._filterPanel);
54
+ this.logger.warning('TEST: HelmBioFilter.init().sync() waitForElementInDom ready');
55
+ this.updateFilterPanel();
56
+ let webEditorHost: HTMLDivElement | null;
57
+ let webEditorApp: IWebEditorApp | null;
58
+ // TODO: Unsubscribe 'click' and 'sizeChanged'
59
+ this.viewSubs.push(fromEvent(this._filterPanel, 'click').subscribe(() => {
60
+ webEditorHost = ui.div();
61
+ webEditorApp = helmHelper.createWebEditorApp(webEditorHost, this.props.substructure);
62
+ const dlg = ui.dialog({showHeader: false, showFooter: true})
63
+ .add(webEditorHost!)
64
+ .onOK(() => {
65
+ try {
66
+ const webEditorValue = webEditorApp!.canvas.getHelm(true)
67
+ .replace(/<\/span>/g, '').replace(/<span style='background:#bbf;'>/g, '');
68
+ this.props = new BioFilterProps(webEditorValue);
69
+ } catch (err: any) {
70
+ this.logger.error(err);
71
+ } finally {
72
+ $(webEditorHost).empty();
73
+ webEditorHost = null;
74
+ webEditorApp = null;
75
+ }
76
+ })
77
+ .onCancel(() => {
78
+ $(webEditorHost).empty();
79
+ webEditorHost = null;
80
+ webEditorApp = null;
81
+ })
82
+ .show({modal: true, fullScreen: true});
83
+ // const onCloseSub = dlg.onClose.subscribe(() => {
84
+ // onCloseSub.unsubscribe();
85
+ // $(editorDiv).empty();
86
+ // editorDiv = undefined;
87
+ // webEditor = undefined;
88
+ // });
89
+ }));
90
+ this.viewSubs.push(ui.onSizeChanged(this._filterPanel).subscribe((_) => {
91
+ try {
92
+ if (!!webEditorApp) {
93
+ const helmString = webEditorApp.canvas.getHelm(true)
94
+ .replace(/<\/span>/g, '').replace(/<span style='background:#bbf;'>/g, '');
95
+ this.updateFilterPanel(helmString);
96
+ }
97
+ } catch (err: any) {
98
+ const [errMsg, errStack] = errInfo(err);
99
+ this.logger.error(errMsg, undefined, errStack);
100
+ }
101
+ }));
102
+ } catch (err: any) {
103
+ const [errMsg, _errStack] = errInfo(err);
104
+ const fp = this._filterPanel;
105
+ fp.innerText = 'error';
106
+ fp.classList.add('d4-error');
107
+ ui.tooltip.bind(fp, errMsg);
108
+ }
109
+ }
110
+
111
+ applyProps() {
112
+ if (!this.helmEditor) return; // helmEditor is not created, the filter is not in dom yet
113
+ this.helmEditor.editor.setHelm(this.props.substructure);
114
+ }
115
+
116
+ get filterPanel() {
117
+ return this._filterPanel;
118
+ }
119
+
120
+ updateFilterPanel(helmString?: string) {
121
+ if (!this.helmEditor) throw new Error('helmEditor is not created, the filter is not in dom yet');
122
+
123
+ const width = this._filterPanel.parentElement!.clientWidth < 100 ? 100 :
124
+ this._filterPanel.parentElement!.clientWidth;
125
+ const height = width / 2;
126
+ if (!helmString) {
127
+ const editDiv = ui.divText('Click to edit', 'helm-substructure-filter');
128
+ updateDivInnerHTML(this._filterPanel, editDiv);
129
+ } else {
130
+ updateDivInnerHTML(this._filterPanel, this.helmEditor.host);
131
+ this.helmEditor.editor.setHelm(helmString);
132
+ this.helmEditor.resizeEditor(width, height);
133
+ }
134
+ }
135
+
136
+ async substructureSearch(column: DG.Column): Promise<DG.BitSet | null> {
137
+ await delay(10);
138
+ const res = await helmSubstructureSearch(this.props.substructure, column);
139
+ return res;
140
+ }
141
+
142
+ // // -- IRenderer --
143
+ //
144
+ // private _onRendered = new Subject<void>();
145
+ //
146
+ // get onRendered(): Observable<void> { return this._onRendered; }
147
+ //
148
+ // invalidate(caller?: string): void {
149
+ // const logPrefix = `${this.viewerToLog()}.invalidate(${caller ? ` <- ${caller} ` : ''})`;
150
+ // this.viewSyncer.sync(logPrefix, async () => { this._onRendered.next(); });
151
+ // }
152
+ //
153
+ // async awaitRendered(timeout: number = 10000): Promise<void> {
154
+ // const callLog = `awaitRendered( ${timeout} )`;
155
+ // const logPrefix = `${this.viewerToLog()}.${callLog}`;
156
+ // await delay(0);
157
+ // await testEvent(this.onRendered, () => {
158
+ // this.logger.debug(`${logPrefix}, ` + '_onRendered event caught');
159
+ // }, () => {
160
+ // this.invalidate(callLog);
161
+ // }, timeout, `${logPrefix} ${timeout} timeout`);
162
+ //
163
+ // // Rethrow stored syncer error (for test purposes)
164
+ // const viewErrors = this.viewSyncer.resetErrors();
165
+ // if (viewErrors.length > 0) throw viewErrors[0];
166
+ // }
167
+ }
168
+
@@ -0,0 +1,131 @@
1
+ import * as ui from 'datagrok-api/ui';
2
+ import * as DG from 'datagrok-api/dg';
3
+ import * as grok from 'datagrok-api/grok';
4
+
5
+ import {Observable, Subject, Unsubscribable} from 'rxjs';
6
+ import {_package} from '../package';
7
+
8
+ export interface IFilterProps {
9
+ get onChanged(): Observable<void>;
10
+
11
+ save(): object;
12
+ apply(propsObj: object): void;
13
+ }
14
+
15
+ /** Fasta and Helm */
16
+ export class BioFilterProps implements IFilterProps {
17
+ private _onChanged: Subject<void> = new Subject<void>();
18
+
19
+ get onChanged(): Observable<void> { return this._onChanged; }
20
+
21
+ constructor(
22
+ public substructure: string
23
+ ) {
24
+ return new Proxy(this, {
25
+ set: (target: any, key: string | symbol, value: any) => {
26
+ _package.logger.debug(`BioFilterProps.set ${key.toString()}( '${value}' )`);
27
+ target[key] = value;
28
+ this._onChanged.next();
29
+ return true;
30
+ }
31
+ });
32
+ }
33
+
34
+ save(): object {
35
+ const propsObj = {};
36
+ for (const [key, value] of Object.entries(this)) {
37
+ if (key !== '_onChanged') {
38
+ // @ts-ignore
39
+ propsObj[key] = this[key];
40
+ }
41
+ }
42
+ return propsObj;
43
+ }
44
+
45
+ apply(propsObj: object) {
46
+ for (const [key, value] of Object.entries(this)) {
47
+ if (key !== '_onChanged') {
48
+ // @ts-ignore
49
+ this[key] = propsObj[key];
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ export interface IBioFilter {
56
+ get type(): string;
57
+
58
+ get props(): IFilterProps;
59
+ set props(value: IFilterProps);
60
+
61
+ get onChanged(): Observable<void>;
62
+ get filterPanel(): HTMLElement;
63
+ get filterSummary(): string;
64
+ get isFiltering(): boolean;
65
+
66
+ attach(): Promise<void>;
67
+ detach(): Promise<void>;
68
+ resetFilter(): void;
69
+ substructureSearch(col: DG.Column): Promise<DG.BitSet | null>;
70
+ }
71
+
72
+ /** Encapsulates input controls handling */
73
+ export abstract class BioFilterBase<TProps extends BioFilterProps> implements IBioFilter {
74
+ abstract get filterPanel(): HTMLElement;
75
+
76
+ abstract get emptyProps(): TProps;
77
+
78
+ onChanged: Subject<void> = new Subject<void>();
79
+
80
+ private _props: TProps;
81
+ protected _propsChanging: boolean = false;
82
+ private _propsOnChangedSub: Unsubscribable | null = null;
83
+
84
+ abstract get type(): string;
85
+
86
+ get props(): TProps {
87
+ if (!this._props) this._props = this.emptyProps;
88
+ return this._props;
89
+ };
90
+
91
+ set props(value: TProps) {
92
+ this._propsChanging = true;
93
+ try {
94
+ if (this._propsOnChangedSub) {
95
+ this._propsOnChangedSub.unsubscribe();
96
+ this._propsOnChangedSub = null;
97
+ }
98
+ this._props = value;
99
+ this.applyProps();
100
+ this.onChanged.next();
101
+ this._propsOnChangedSub = this._props.onChanged
102
+ .subscribe(() => {
103
+ this.onChanged.next();
104
+ });
105
+ } finally {
106
+ this._propsChanging = false;
107
+ }
108
+ };
109
+
110
+ abstract attach(): Promise<void>;
111
+
112
+ async detach(): Promise<void> {
113
+ if (this._propsOnChangedSub) {
114
+ this._propsOnChangedSub.unsubscribe();
115
+ this._propsOnChangedSub = null;
116
+ }
117
+ }
118
+
119
+ abstract applyProps(): void;
120
+
121
+ get filterSummary(): string { return this.props.substructure; };
122
+
123
+ get isFiltering(): boolean { return this.props.substructure !== ''; }
124
+
125
+ abstract substructureSearch(_column: DG.Column): Promise<DG.BitSet | null>;
126
+
127
+ resetFilter(): void {
128
+ this.props = this.emptyProps;
129
+ this.onChanged.next();
130
+ }
131
+ }