@datagrok/sequence-translator 1.2.7 → 1.3.0
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/.eslintrc.json +5 -5
- package/CHANGELOG.md +14 -0
- package/dist/package-test.js +2 -1
- package/dist/package-test.js.LICENSE.txt +8 -0
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +2 -1
- package/dist/package.js.LICENSE.txt +8 -0
- package/dist/package.js.map +1 -1
- package/files/pattern-app-data.json +80 -0
- package/package.json +22 -14
- package/src/{model → apps/common/model}/const.ts +1 -1
- package/src/{model/data-loading-utils → apps/common/model/data-loader}/const.ts +7 -2
- package/src/apps/common/model/data-loader/json-loader.ts +48 -0
- package/src/{model/data-loading-utils → apps/common/model/data-loader}/types.ts +13 -6
- package/src/{model → apps/common/model}/monomer-lib/lib-wrapper.ts +9 -12
- package/src/apps/common/model/oligo-toolkit-package.ts +30 -0
- package/src/{model → apps/common/model}/parsing-validation/format-detector.ts +5 -5
- package/src/{model → apps/common/model}/parsing-validation/format-handler.ts +18 -19
- package/src/{model → apps/common/model}/parsing-validation/sequence-validator.ts +1 -1
- package/src/apps/common/view/app-ui-base.ts +28 -0
- package/src/apps/common/view/combined-app-ui.ts +66 -0
- package/src/{view/utils → apps/common/view/components}/colored-input/colored-text-input.ts +1 -1
- package/src/{view/utils → apps/common/view/components}/draw-molecule.ts +1 -1
- package/src/{view/utils → apps/common/view/components}/molecule-img.ts +3 -3
- package/src/{view/const/ui.ts → apps/common/view/const.ts} +4 -4
- package/src/apps/common/view/isolated-app-ui.ts +43 -0
- package/src/{view/monomer-lib-viewer/viewer.ts → apps/common/view/monomer-lib-viewer.ts} +2 -2
- package/src/apps/common/view/utils.ts +29 -0
- package/src/apps/pattern/model/const.ts +121 -0
- package/src/apps/pattern/model/data-manager.ts +297 -0
- package/src/apps/pattern/model/event-bus.ts +487 -0
- package/src/apps/pattern/model/router.ts +46 -0
- package/src/apps/pattern/model/subscription-manager.ts +21 -0
- package/src/apps/pattern/model/translator.ts +94 -0
- package/src/apps/pattern/model/types.ts +52 -0
- package/src/apps/pattern/model/utils.ts +110 -0
- package/src/apps/pattern/view/components/bulk-convert/column-input.ts +79 -0
- package/src/apps/pattern/view/components/bulk-convert/table-controls.ts +38 -0
- package/src/apps/pattern/view/components/bulk-convert/table-input.ts +95 -0
- package/src/apps/pattern/view/components/edit-block-controls.ts +196 -0
- package/src/apps/pattern/view/components/left-section.ts +44 -0
- package/src/apps/pattern/view/components/load-block-controls.ts +200 -0
- package/src/apps/pattern/view/components/numeric-label-visibility-controls.ts +69 -0
- package/src/apps/pattern/view/components/right-section.ts +148 -0
- package/src/apps/pattern/view/components/strand-editor/dialog.ts +79 -0
- package/src/apps/pattern/view/components/strand-editor/header-controls.ts +105 -0
- package/src/apps/pattern/view/components/strand-editor/strand-controls.ts +159 -0
- package/src/apps/pattern/view/components/terminal-modification-editor.ts +127 -0
- package/src/apps/pattern/view/components/translation-examples-block.ts +139 -0
- package/src/{view/style/pattern-app.css → apps/pattern/view/style.css} +4 -0
- package/src/apps/pattern/view/svg-utils/const.ts +77 -0
- package/src/apps/pattern/view/svg-utils/legend-block.ts +92 -0
- package/src/apps/pattern/view/svg-utils/strands-block.ts +335 -0
- package/src/apps/pattern/view/svg-utils/svg-block-base.ts +37 -0
- package/src/apps/pattern/view/svg-utils/svg-display-manager.ts +44 -0
- package/src/apps/pattern/view/svg-utils/svg-element-factory.ts +94 -0
- package/src/apps/pattern/view/svg-utils/svg-renderer.ts +51 -0
- package/src/apps/pattern/view/svg-utils/text-dimensions-calculator.ts +29 -0
- package/src/apps/pattern/view/svg-utils/title-block.ts +53 -0
- package/src/apps/pattern/view/svg-utils/utils.ts +37 -0
- package/src/apps/pattern/view/types.ts +14 -0
- package/src/apps/pattern/view/ui.ts +61 -0
- package/src/{model/structure-app → apps/structure/model}/mol-transformations.ts +3 -3
- package/src/{model/structure-app → apps/structure/model}/monomer-code-parser.ts +9 -10
- package/src/{model/structure-app → apps/structure/model}/oligo-structure.ts +4 -4
- package/src/{model/structure-app → apps/structure/model}/sequence-to-molfile.ts +2 -2
- package/src/{view/apps/oligo-structure.ts → apps/structure/view/ui.ts} +31 -17
- package/src/{model/translator-app → apps/translator/model}/conversion-utils.ts +25 -7
- package/src/{model/translator-app → apps/translator/model}/format-converter.ts +7 -12
- package/src/{view/const/oligo-translator.ts → apps/translator/view/const.ts} +1 -1
- package/src/{view/apps/oligo-translator.ts → apps/translator/view/ui.ts} +88 -42
- package/src/demo/demo-st-ui.ts +12 -32
- package/src/package.ts +91 -55
- package/src/plugins/mermade.ts +9 -9
- package/src/polytool/const.ts +28 -0
- package/src/polytool/csv-to-json-monomer-lib-converter.ts +40 -0
- package/src/polytool/cyclized.ts +56 -0
- package/src/polytool/monomer-lib-handler.ts +115 -0
- package/src/polytool/pt-conversion.ts +307 -0
- package/src/polytool/pt-dialog.ts +115 -0
- package/src/polytool/pt-enumeration.ts +127 -0
- package/src/polytool/pt-rules.ts +73 -0
- package/src/polytool/utils.ts +27 -0
- package/src/tests/const.ts +5 -5
- package/src/tests/formats-support.ts +6 -6
- package/src/tests/formats-to-helm.ts +5 -5
- package/src/tests/helm-to-nucleotides.ts +5 -10
- package/tsconfig.json +3 -9
- package/webpack.config.js +3 -0
- package/files/axolabs-style.json +0 -97
- package/src/model/data-loading-utils/json-loader.ts +0 -38
- package/src/model/pattern-app/const.ts +0 -33
- package/src/model/pattern-app/draw-svg.ts +0 -193
- package/src/model/pattern-app/helpers.ts +0 -96
- package/src/model/pattern-app/oligo-pattern.ts +0 -111
- package/src/view/app-ui.ts +0 -193
- package/src/view/apps/oligo-pattern.ts +0 -759
- /package/src/{model → apps/common/model}/helpers.ts +0 -0
- /package/src/{model → apps/common/model}/monomer-lib/const.ts +0 -0
- /package/src/{view/utils → apps/common/view/components}/app-info-dialog.ts +0 -0
- /package/src/{view/utils → apps/common/view/components}/colored-input/input-painters.ts +0 -0
- /package/src/{view/style/colored-text-input.css → apps/common/view/components/colored-input/style.css} +0 -0
- /package/src/{view/utils → apps/common/view/components}/router.ts +0 -0
- /package/src/{model/structure-app → apps/structure/model}/const.ts +0 -0
- /package/src/{view/style/structure-app.css → apps/structure/view/style.css} +0 -0
- /package/src/{model/translator-app → apps/translator/model}/const.ts +0 -0
- /package/src/{view/style/translator-app.css → apps/translator/view/style.css} +0 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
|
|
4
|
+
import {SubscriptionManager} from '../../model/subscription-manager';
|
|
5
|
+
|
|
6
|
+
import '../style.css';
|
|
7
|
+
|
|
8
|
+
import {StringInput} from '../types';
|
|
9
|
+
|
|
10
|
+
import $ from 'cash-dom';
|
|
11
|
+
import {DataManager} from '../../model/data-manager';
|
|
12
|
+
import {EventBus} from '../../model/event-bus';
|
|
13
|
+
|
|
14
|
+
export class PatternLoadControlsManager {
|
|
15
|
+
private subscriptions = new SubscriptionManager();
|
|
16
|
+
private authorSelectedByUser = false;
|
|
17
|
+
|
|
18
|
+
constructor(
|
|
19
|
+
private eventBus: EventBus,
|
|
20
|
+
private dataManager: DataManager
|
|
21
|
+
) {
|
|
22
|
+
this.eventBus.patternLoadRequested$.subscribe((patternHash: string) => this.handlePatternChoice(patternHash));
|
|
23
|
+
|
|
24
|
+
this.eventBus.patternDeletionRequested$.subscribe(async (patternName: string) => {
|
|
25
|
+
await this.dataManager.deletePattern(patternName, this.eventBus);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async handlePatternChoice(patternHash: string): Promise<void> {
|
|
30
|
+
let patternConfiguration = await this.dataManager.getPatternConfig(patternHash);
|
|
31
|
+
if (patternConfiguration === null)
|
|
32
|
+
patternConfiguration = this.dataManager.getDefaultPatternConfig();
|
|
33
|
+
|
|
34
|
+
this.eventBus.setPatternConfig(patternConfiguration);
|
|
35
|
+
this.eventBus.updateControlsUponPatternLoaded(patternHash);
|
|
36
|
+
this.eventBus.setLastLoadedPatternConfig(patternConfiguration);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private isCurrentUserSelected(): boolean {
|
|
40
|
+
return this.eventBus.getSelectedAuthor() !== this.dataManager.getOtherUsersAuthorshipCategory();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
createControls(): HTMLElement[] {
|
|
44
|
+
const inputsContainer = this.getPatternInputsContainer();
|
|
45
|
+
return [
|
|
46
|
+
ui.h1('Load'),
|
|
47
|
+
inputsContainer,
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private getPatternInputsContainer(): HTMLDivElement {
|
|
52
|
+
const inputsContainer = ui.divH(this.createPatternInputs());
|
|
53
|
+
|
|
54
|
+
this.eventBus.patternListUpdated$.subscribe(() => {
|
|
55
|
+
this.subscriptions.unsubscribeAll();
|
|
56
|
+
|
|
57
|
+
// todo: change values of selectedPattern and selectedUser here
|
|
58
|
+
|
|
59
|
+
$(inputsContainer).empty();
|
|
60
|
+
$(inputsContainer).append(this.createPatternInputs());
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return inputsContainer;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private createPatternInputs(): HTMLElement[] {
|
|
67
|
+
const authorChoiceInput = this.createAuthorChoiceInput();
|
|
68
|
+
const patternChoiceInputContainer = this.createPatternChoiceInputContainer();
|
|
69
|
+
const deletePatternButton = this.createDeletePatternButton();
|
|
70
|
+
|
|
71
|
+
return [
|
|
72
|
+
authorChoiceInput.root,
|
|
73
|
+
patternChoiceInputContainer,
|
|
74
|
+
deletePatternButton
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private createPatternChoiceInputContainer(): HTMLDivElement {
|
|
79
|
+
const patternChoiceInput = this.createPatternChoiceInput();
|
|
80
|
+
const patternChoiceInputContainer = ui.div([patternChoiceInput.root]);
|
|
81
|
+
|
|
82
|
+
const subscription = this.eventBus.userSelection$.subscribe(() => {
|
|
83
|
+
$(patternChoiceInputContainer).empty();
|
|
84
|
+
$(patternChoiceInputContainer).append(this.createPatternChoiceInput().root);
|
|
85
|
+
});
|
|
86
|
+
this.subscriptions.add(subscription);
|
|
87
|
+
|
|
88
|
+
return patternChoiceInputContainer;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private createAuthorChoiceInput(): StringInput {
|
|
92
|
+
const possibleValues = [this.dataManager.getCurrentUserAuthorshipCategory()];
|
|
93
|
+
if (this.dataManager.getOtherUsersPatternNames().length > 0)
|
|
94
|
+
possibleValues.push(this.dataManager.getOtherUsersAuthorshipCategory());
|
|
95
|
+
|
|
96
|
+
const authorChoiceInput = ui.choiceInput(
|
|
97
|
+
'Author',
|
|
98
|
+
this.eventBus.getSelectedAuthor(),
|
|
99
|
+
possibleValues,
|
|
100
|
+
(userName: string) => {
|
|
101
|
+
this.authorSelectedByUser = true;
|
|
102
|
+
this.eventBus.selectAuthor(userName);
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
this.setAuthorChoiceInputStyle(authorChoiceInput);
|
|
106
|
+
authorChoiceInput.setTooltip('Select pattern author');
|
|
107
|
+
|
|
108
|
+
return authorChoiceInput;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private setAuthorChoiceInputStyle(userChoiceInput: StringInput): void {
|
|
112
|
+
$(userChoiceInput.input).css({
|
|
113
|
+
'max-width': '100px',
|
|
114
|
+
'min-width': '100px',
|
|
115
|
+
});
|
|
116
|
+
$(userChoiceInput.root).css({
|
|
117
|
+
'padding-right': '30px',
|
|
118
|
+
'padding-left': '30px',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private createPatternChoiceInput(): StringInput {
|
|
123
|
+
const patternList = this.isCurrentUserSelected() ?
|
|
124
|
+
this.dataManager.getCurrentUserPatternNames() :
|
|
125
|
+
this.dataManager.getOtherUsersPatternNames();
|
|
126
|
+
|
|
127
|
+
if (this.authorSelectedByUser) {
|
|
128
|
+
const patternHash = this.dataManager.getPatternHash(patternList[0], this.isCurrentUserSelected());
|
|
129
|
+
this.eventBus.requestPatternLoad(patternHash);
|
|
130
|
+
this.eventBus.updateUrlState(patternHash);
|
|
131
|
+
this.authorSelectedByUser = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const defaultValue = this.getPatternName(patternList);
|
|
135
|
+
const choiceInput = ui.choiceInput('Pattern', defaultValue, patternList);
|
|
136
|
+
choiceInput.setTooltip('Select pattern to load');
|
|
137
|
+
|
|
138
|
+
$(choiceInput.input).css({
|
|
139
|
+
'max-width': '100px',
|
|
140
|
+
'min-width': '100px',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.subscriptions.add(
|
|
144
|
+
choiceInput.onInput(
|
|
145
|
+
() => {
|
|
146
|
+
const patternHash = this.dataManager.getPatternHash(choiceInput.value!, this.isCurrentUserSelected());
|
|
147
|
+
this.eventBus.requestPatternLoad(patternHash);
|
|
148
|
+
this.eventBus.updateUrlState(patternHash);
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
this.subscriptions.add(
|
|
154
|
+
this.eventBus.patternLoaded$.subscribe(() => {
|
|
155
|
+
const patternName = this.eventBus.getPatternName();
|
|
156
|
+
if (choiceInput.value !== patternName)
|
|
157
|
+
choiceInput.value = this.getPatternName(patternList);
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return choiceInput;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getPatternName(patternList: string[]): string {
|
|
165
|
+
return patternList.find(
|
|
166
|
+
(patternName) => patternName === this.eventBus.getPatternName()
|
|
167
|
+
) ?? patternList[0];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private createDeletePatternButton(): HTMLButtonElement {
|
|
171
|
+
const button = ui.button(
|
|
172
|
+
ui.iconFA('trash-alt'),
|
|
173
|
+
() => {
|
|
174
|
+
if (this.eventBus.getPatternName() === this.dataManager.getDefaultPatternName()) {
|
|
175
|
+
grok.shell.warning('Cannot delete example pattern');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.showDeletePatternDialog();
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
ui.tooltip.bind(button, 'Delete pattern from user storage');
|
|
183
|
+
|
|
184
|
+
const subscription = this.eventBus.userSelection$.subscribe(() => {
|
|
185
|
+
// $(button).toggle(this.isCurrentUserSelected());
|
|
186
|
+
button.disabled = !this.isCurrentUserSelected();
|
|
187
|
+
});
|
|
188
|
+
this.subscriptions.add(subscription);
|
|
189
|
+
|
|
190
|
+
return button;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private showDeletePatternDialog(): void {
|
|
194
|
+
const dialog = ui.dialog('Delete pattern');
|
|
195
|
+
const patternName = this.eventBus.getPatternName();
|
|
196
|
+
dialog.add(ui.divText(`Are you sure you want to delete pattern ${patternName}?`));
|
|
197
|
+
dialog.onOK(() => this.eventBus.requestPatternDeletion(patternName));
|
|
198
|
+
dialog.show();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as ui from 'datagrok-api/ui';
|
|
2
|
+
|
|
3
|
+
import {EventBus} from '../../model/event-bus';
|
|
4
|
+
import {isOverhangNucleotide} from '../../model/utils';
|
|
5
|
+
import {BooleanInput} from '../types';
|
|
6
|
+
|
|
7
|
+
import $ from 'cash-dom';
|
|
8
|
+
|
|
9
|
+
/** Toggles for numeric labels over nucleotides in SVG */
|
|
10
|
+
export class NumericLabelVisibilityControls {
|
|
11
|
+
private togglesContainer: HTMLDivElement = ui.div([]);
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private eventBus: EventBus
|
|
15
|
+
) {
|
|
16
|
+
this.eventBus.uniqueNucleotidesChanged$().subscribe(() => {
|
|
17
|
+
this.updateContainer();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getContainer(): HTMLDivElement {
|
|
22
|
+
return this.togglesContainer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private updateContainer(): void {
|
|
26
|
+
$(this.togglesContainer).empty();
|
|
27
|
+
$(this.togglesContainer).append(this.createInputs());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private createInputs(): HTMLDivElement {
|
|
31
|
+
const uniqueNucleotideBases = this.eventBus.getUniqueNucleotides();
|
|
32
|
+
const nucleotidesWithoutOverhangs = uniqueNucleotideBases.filter((n) => !isOverhangNucleotide(n));
|
|
33
|
+
|
|
34
|
+
const inputBases = nucleotidesWithoutOverhangs.map(
|
|
35
|
+
(nucleotide: string) => this.createSingleInput(nucleotide)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
inputBases.sort(
|
|
39
|
+
(inputA, inputB) => inputA.captionLabel.textContent!.localeCompare(inputB.captionLabel.textContent!)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return ui.divH(inputBases.map((input) => input.root));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private createSingleInput(nucleotide: string): BooleanInput {
|
|
46
|
+
const initialValue = this.eventBus.getModificationsWithNumericLabels().includes(nucleotide);
|
|
47
|
+
const input = ui.boolInput(
|
|
48
|
+
nucleotide,
|
|
49
|
+
initialValue,
|
|
50
|
+
(value: boolean) => this.handleNumericLabelToggle(nucleotide, value)
|
|
51
|
+
);
|
|
52
|
+
$(input.root).css('padding-right', '20px');
|
|
53
|
+
|
|
54
|
+
input.setTooltip(`Show numeric labels for ${nucleotide}`);
|
|
55
|
+
|
|
56
|
+
return input;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private handleNumericLabelToggle(nucleotide: string, isVisible: boolean): void {
|
|
60
|
+
const labelledNucleotides = this.eventBus.getModificationsWithNumericLabels();
|
|
61
|
+
const hasNumericLabel = labelledNucleotides.includes(nucleotide);
|
|
62
|
+
if (hasNumericLabel === isVisible)
|
|
63
|
+
return;
|
|
64
|
+
|
|
65
|
+
const newArray = isVisible ? labelledNucleotides.concat(nucleotide) :
|
|
66
|
+
labelledNucleotides.filter((n) => n !== nucleotide);
|
|
67
|
+
this.eventBus.updateModificationsWithNumericLabels(newArray);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import * as grok from 'datagrok-api/grok';
|
|
2
|
+
import * as ui from 'datagrok-api/ui';
|
|
3
|
+
|
|
4
|
+
import {EventBus} from '../../model/event-bus';
|
|
5
|
+
import {DataManager} from '../../model/data-manager';
|
|
6
|
+
import {PatternExistsError, PatternNameExistsError} from '../../model/types';
|
|
7
|
+
import {SvgDisplayManager} from '../svg-utils/svg-display-manager';
|
|
8
|
+
import {NumericLabelVisibilityControls} from './numeric-label-visibility-controls';
|
|
9
|
+
import {TranslationExamplesBlock} from './translation-examples-block';
|
|
10
|
+
|
|
11
|
+
export class PatternAppRightSection {
|
|
12
|
+
private svgDisplay: HTMLDivElement;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private eventBus: EventBus,
|
|
16
|
+
private dataManager: DataManager
|
|
17
|
+
) {
|
|
18
|
+
this.svgDisplay = SvgDisplayManager.createSvgDiv(eventBus);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
getLayout(): HTMLDivElement {
|
|
22
|
+
const numericLabelTogglesContainer = new NumericLabelVisibilityControls(this.eventBus).getContainer();
|
|
23
|
+
const downloadAndEditControls = this.generateDownloadControls();
|
|
24
|
+
const translationExamplesContainer = new TranslationExamplesBlock(
|
|
25
|
+
this.eventBus, this.dataManager
|
|
26
|
+
).createContainer();
|
|
27
|
+
const layout = ui.panel([
|
|
28
|
+
this.svgDisplay,
|
|
29
|
+
numericLabelTogglesContainer,
|
|
30
|
+
downloadAndEditControls,
|
|
31
|
+
translationExamplesContainer,
|
|
32
|
+
// ui.h1('Additional modifications'),
|
|
33
|
+
// ui.form([
|
|
34
|
+
// terminalModificationInputs[STRAND.SENSE][FIVE_PRIME_END],
|
|
35
|
+
// terminalModificationInputs[STRAND.SENSE][THREE_PRIME_END],
|
|
36
|
+
// ]),
|
|
37
|
+
// asModificationDiv,
|
|
38
|
+
], {style: {overflowX: 'scroll', padding: '12px 24px'}});
|
|
39
|
+
return layout;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private generateDownloadControls(): HTMLDivElement {
|
|
43
|
+
return ui.divH([
|
|
44
|
+
this.createSavePatternButton(),
|
|
45
|
+
this.createDownloadPngButton(),
|
|
46
|
+
this.createShareLinkButton(),
|
|
47
|
+
], {style: {gap: '12px', marginTop: '12px'}});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private createDownloadPngButton(): HTMLButtonElement {
|
|
51
|
+
const svgDownloadButton = ui.button('Get PNG', () => this.eventBus.requestSvgSave());
|
|
52
|
+
|
|
53
|
+
ui.tooltip.bind(svgDownloadButton, 'Download pattern as PNG');
|
|
54
|
+
|
|
55
|
+
return svgDownloadButton;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private createShareLinkButton(): HTMLButtonElement {
|
|
59
|
+
const shareLinkButton = ui.button(
|
|
60
|
+
ui.iconFA('link'),
|
|
61
|
+
() => navigator.clipboard.writeText(window.location.href)
|
|
62
|
+
.then(() => grok.shell.info('Link to pattern copied to clipboard'))
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
this.eventBus.patternHasUnsavedChanges$.subscribe((hasUnsavedChanges: boolean) => {
|
|
66
|
+
shareLinkButton.disabled = hasUnsavedChanges;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
ui.tooltip.bind(shareLinkButton, 'Share pattern link');
|
|
70
|
+
return shareLinkButton;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private createSavePatternButton(): HTMLButtonElement {
|
|
74
|
+
const savePatternButton = ui.button('Save', () => this.processSaveButtonClick());
|
|
75
|
+
|
|
76
|
+
this.eventBus.patternHasUnsavedChanges$.subscribe((hasUnsavedChanges: boolean) => {
|
|
77
|
+
savePatternButton.disabled = !hasUnsavedChanges;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ui.tooltip.bind(savePatternButton, 'Save pattern to user storage');
|
|
81
|
+
|
|
82
|
+
return savePatternButton;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private processSaveButtonClick(): void {
|
|
86
|
+
const patternName = this.eventBus.getPatternName();
|
|
87
|
+
if (patternName === this.dataManager.getDefaultPatternName()) {
|
|
88
|
+
grok.shell.warning(`Cannot save default pattern`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (patternName === '') {
|
|
92
|
+
grok.shell.warning(`Insert pattern name`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.dataManager.savePatternToUserStorage(this.eventBus)
|
|
96
|
+
.then(() => {
|
|
97
|
+
grok.shell.info(`Pattern ${patternName} saved`);
|
|
98
|
+
})
|
|
99
|
+
.catch((e) => this.handleErrorWhileSavingPattern(e));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private handleErrorWhileSavingPattern(e: Error): void {
|
|
103
|
+
if (e instanceof PatternNameExistsError) {
|
|
104
|
+
new OverwritePatternDialog(this.eventBus, this.dataManager).show();
|
|
105
|
+
} else if (e instanceof PatternExistsError) {
|
|
106
|
+
grok.shell.warning(ui.div([
|
|
107
|
+
ui.divText(`Pattern already exists`),
|
|
108
|
+
ui.button('Load', () => {
|
|
109
|
+
const hash = e.message;
|
|
110
|
+
this.eventBus.requestLoadPatternInNewTab(hash);
|
|
111
|
+
}),
|
|
112
|
+
]));
|
|
113
|
+
} else {
|
|
114
|
+
console.error('Error while saving pattern', e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
class OverwritePatternDialog {
|
|
120
|
+
constructor(
|
|
121
|
+
private eventBus: EventBus,
|
|
122
|
+
private dataManager: DataManager
|
|
123
|
+
) { }
|
|
124
|
+
|
|
125
|
+
show(): void {
|
|
126
|
+
const patternName = this.eventBus.getPatternName();
|
|
127
|
+
const dialog = ui.dialog(`Pattern "${patternName}" already exists`);
|
|
128
|
+
dialog.add(ui.divText(
|
|
129
|
+
`Pattern "${patternName}" already exists. Do you want to overwrite it?`
|
|
130
|
+
));
|
|
131
|
+
dialog.show();
|
|
132
|
+
|
|
133
|
+
dialog.onOK(() => this.processOverwriteNamesakePattern());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private processOverwriteNamesakePattern(): void {
|
|
137
|
+
const patternName = this.eventBus.getPatternName();
|
|
138
|
+
this.dataManager.overwritePatternInUserStorage(this.eventBus)
|
|
139
|
+
.then(() => {
|
|
140
|
+
grok.shell.info(`Pattern ${patternName} overwritten`);
|
|
141
|
+
})
|
|
142
|
+
.catch((e) => {
|
|
143
|
+
console.error('Error while overwriting pattern in user storage', e);
|
|
144
|
+
grok.shell.error('Error while overwriting pattern');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/* Do not change these import lines to match external modules in webpack configuration */
|
|
2
|
+
import * as DG from 'datagrok-api/dg';
|
|
3
|
+
import * as ui from 'datagrok-api/ui';
|
|
4
|
+
|
|
5
|
+
import $ from 'cash-dom';
|
|
6
|
+
import _ from 'lodash';
|
|
7
|
+
|
|
8
|
+
import {EventBus} from '../../../model/event-bus';
|
|
9
|
+
import {PatternConfiguration} from '../../../model/types';
|
|
10
|
+
import {HeaderControls} from './header-controls';
|
|
11
|
+
import {StrandControls} from './strand-controls';
|
|
12
|
+
import {SubscriptionManager} from '../../../model/subscription-manager';
|
|
13
|
+
import {DataManager} from '../../../model/data-manager';
|
|
14
|
+
|
|
15
|
+
export class StrandEditorDialog {
|
|
16
|
+
private static isDialogOpen = false;
|
|
17
|
+
private static instance: StrandEditorDialog;
|
|
18
|
+
|
|
19
|
+
private initialPatternConfig: PatternConfiguration;
|
|
20
|
+
private subscriptions = new SubscriptionManager();
|
|
21
|
+
|
|
22
|
+
private constructor(
|
|
23
|
+
private eventBus: EventBus,
|
|
24
|
+
private dataManager: DataManager
|
|
25
|
+
) { }
|
|
26
|
+
|
|
27
|
+
static open(eventBus: EventBus, dataManager: DataManager): void {
|
|
28
|
+
if (StrandEditorDialog.isDialogOpen)
|
|
29
|
+
return;
|
|
30
|
+
|
|
31
|
+
if (!StrandEditorDialog.instance)
|
|
32
|
+
StrandEditorDialog.instance = new StrandEditorDialog(eventBus, dataManager);
|
|
33
|
+
|
|
34
|
+
StrandEditorDialog.instance.openDialog();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private openDialog(): void {
|
|
38
|
+
this.initialPatternConfig = _.cloneDeep(this.eventBus.getPatternConfig());
|
|
39
|
+
StrandEditorDialog.isDialogOpen = true;
|
|
40
|
+
this.createDialog().show();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private createDialog(): DG.Dialog {
|
|
44
|
+
const editorBody = ui.divV([]);
|
|
45
|
+
this.subscriptions.add(
|
|
46
|
+
this.eventBus.strandsUpdated$.subscribe(() => this.onStrandsUpdated(editorBody))
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const dialog = ui.dialog('Edit strands')
|
|
50
|
+
.add(editorBody)
|
|
51
|
+
.onOK(() => {})
|
|
52
|
+
.onCancel(() => this.resetToInitialState());
|
|
53
|
+
|
|
54
|
+
this.subscriptions.add(
|
|
55
|
+
dialog.onClose.subscribe(() => {
|
|
56
|
+
StrandEditorDialog.isDialogOpen = false;
|
|
57
|
+
this.subscriptions.unsubscribeAll();
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return dialog;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private onStrandsUpdated(editorBody: HTMLDivElement) {
|
|
65
|
+
const headerControls = new HeaderControls(
|
|
66
|
+
this.eventBus, this.initialPatternConfig, this.subscriptions
|
|
67
|
+
).create();
|
|
68
|
+
const strandControls = new StrandControls(this.eventBus, this.dataManager, this.subscriptions).create();
|
|
69
|
+
|
|
70
|
+
$(editorBody).empty();
|
|
71
|
+
$(editorBody).append(headerControls, strandControls);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private resetToInitialState(): void {
|
|
75
|
+
// this.eventBus.setLastLoadedPatternConfig(this.initialPatternConfig);
|
|
76
|
+
this.eventBus.setPatternConfig(this.initialPatternConfig);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as ui from 'datagrok-api/ui';
|
|
2
|
+
|
|
3
|
+
import {STRAND, STRANDS} from '../../../model/const';
|
|
4
|
+
import {EventBus} from '../../../model/event-bus';
|
|
5
|
+
import {PatternConfiguration, PhosphorothioateLinkageFlags} from '../../../model/types';
|
|
6
|
+
import {BooleanInput} from '../../types';
|
|
7
|
+
import {SubscriptionManager} from '../../../model/subscription-manager';
|
|
8
|
+
|
|
9
|
+
export class HeaderControls {
|
|
10
|
+
constructor(
|
|
11
|
+
private eventBus: EventBus,
|
|
12
|
+
private initialPatternConfig: PatternConfiguration,
|
|
13
|
+
private subscriptions: SubscriptionManager
|
|
14
|
+
) { }
|
|
15
|
+
|
|
16
|
+
create(): HTMLDivElement {
|
|
17
|
+
const container = ui.divV([
|
|
18
|
+
ui.h1('PTO'),
|
|
19
|
+
ui.divH([
|
|
20
|
+
this.createAllPtoActivationInput().root,
|
|
21
|
+
...this.createFirstPtoInputs().map((input) => input.root),
|
|
22
|
+
], {style: {gap: '12px'}})
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
return container;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private areAllPtoLinkagesSet(flags: PhosphorothioateLinkageFlags): boolean {
|
|
29
|
+
const totalNumberOfPTOFlags = STRANDS.map(
|
|
30
|
+
(strand) => flags[strand].filter((flag) => flag).length
|
|
31
|
+
)
|
|
32
|
+
.reduce((a, b) => a + b, 0);
|
|
33
|
+
|
|
34
|
+
const totalNumberOfNucleotides = STRANDS.map(
|
|
35
|
+
(strand) => this.initialPatternConfig.nucleotideSequences[strand].length
|
|
36
|
+
).reduce((a, b) => a + b, 0);
|
|
37
|
+
|
|
38
|
+
// There are +1 more PTO flags in each strand than there are nucleotides
|
|
39
|
+
const addendum = STRANDS.filter((strand) => flags[strand].length).length;
|
|
40
|
+
return totalNumberOfPTOFlags === totalNumberOfNucleotides + addendum;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private createAllPtoActivationInput(): BooleanInput {
|
|
44
|
+
const flags = this.initialPatternConfig.phosphorothioateLinkageFlags;
|
|
45
|
+
const initialValue = this.areAllPtoLinkagesSet(flags);
|
|
46
|
+
const allPtoActivationInput = ui.boolInput('All PTO', initialValue);
|
|
47
|
+
|
|
48
|
+
allPtoActivationInput.onInput(() => {
|
|
49
|
+
const value = allPtoActivationInput.value!;
|
|
50
|
+
this.eventBus.setAllPTOLinkages(value);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const subscription = this.eventBus.phosphorothioateLingeFlagsChanged$.subscribe(() => {
|
|
54
|
+
const flags = this.eventBus.getPatternConfig().phosphorothioateLinkageFlags;
|
|
55
|
+
const newValue = this.areAllPtoLinkagesSet(flags);
|
|
56
|
+
allPtoActivationInput.value = newValue;
|
|
57
|
+
});
|
|
58
|
+
this.subscriptions.add(subscription);
|
|
59
|
+
|
|
60
|
+
this.addStyleToPtoInput(allPtoActivationInput);
|
|
61
|
+
ui.tooltip.bind(allPtoActivationInput.captionLabel, 'Activate all phosphothioates');
|
|
62
|
+
|
|
63
|
+
return allPtoActivationInput;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private addStyleToPtoInput(allPtoActivationInput: BooleanInput): void {
|
|
67
|
+
const label = allPtoActivationInput.captionLabel;
|
|
68
|
+
label.classList.add('ui-label-right');
|
|
69
|
+
Object.assign(label.style, {
|
|
70
|
+
textAlign: 'left',
|
|
71
|
+
maxWidth: '100px',
|
|
72
|
+
minWidth: '40px',
|
|
73
|
+
width: 'auto'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private createFirstPtoInputs(): BooleanInput [] {
|
|
78
|
+
return STRANDS.map((strand) => {
|
|
79
|
+
if (!this.eventBus.isAntisenseStrandActive() && strand === STRAND.ANTISENSE)
|
|
80
|
+
return;
|
|
81
|
+
const initialValue = this.isFirstPtoActive(strand);
|
|
82
|
+
const firstPtoInput = ui.boolInput(`First ${strand} PTO`, initialValue);
|
|
83
|
+
|
|
84
|
+
firstPtoInput.onInput(() => {
|
|
85
|
+
const value = firstPtoInput.value!;
|
|
86
|
+
this.eventBus.setPhosphorothioateLinkageFlag(strand, 0, value);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const subscription = this.eventBus.phosphorothioateLingeFlagsChanged$.subscribe((flags) => {
|
|
90
|
+
const newValue = flags[strand][0];
|
|
91
|
+
firstPtoInput.value = newValue;
|
|
92
|
+
});
|
|
93
|
+
this.subscriptions.add(subscription);
|
|
94
|
+
|
|
95
|
+
this.addStyleToPtoInput(firstPtoInput);
|
|
96
|
+
ui.tooltip.bind(firstPtoInput.captionLabel, `Activate first phosphothioate in ${strand}`);
|
|
97
|
+
return firstPtoInput;
|
|
98
|
+
}).filter((input) => input !== undefined) as BooleanInput[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private isFirstPtoActive(strand: STRAND): boolean {
|
|
102
|
+
return this.initialPatternConfig.phosphorothioateLinkageFlags[strand][0];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|