diff --git a/README.md b/README.md index 057829ee..4a7c9d25 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ It is necessary to install and configure the following dependencies: After installing Node, you should be able to run the following command to install development tools. You will only need to run this command when dependencies change in [package.json](package.json). - npm install + yarn install ### Prepare Firebase Realtime Database 1. Go to [Firebase Console] and login with your Google account. diff --git a/codedoc/CODEDOC.md b/codedoc/CODEDOC.md new file mode 100644 index 00000000..e21feecf --- /dev/null +++ b/codedoc/CODEDOC.md @@ -0,0 +1,80 @@ +# Code Tutorial +## Configuration Files +**environment.ts**: The config file used to connect firebase realtime database, set oncotree version and `dev/prod` configs. +**firebase.json**: The firebase connection auth file generated by Google API service, used to connect Firebase in backend. +**heroku-mongo.yml**: The mongo connection setting which should be copied and pasted in `application-prod.yml/application-dev.yml`. +**token**: The trial curation API authentication token. + +## File Types +In the trial curation platform, the frontend code root directory is `/matchminer-curate/src/main/webapp/app`. +Each folder is a component or service folder. Services only have `*.service.ts` files which can be thought as +utility classes and used in components. + +Most components have 4 files: +``` + *.component.html: includes all html related code. + *.component.ts: contains functions used in *.component.html. + *.model.ts: defines data structure of objects used in *.component.ts. + *.scss: writes all css classes used in *.component.html. +``` + +## Components + +### Meta(jhi-meta) +Display trial metadata for all trials in a table. A trial meta record will be generated automatically when every time +users import a new trial from cancer.gov, which is implemented in `importTrialsFromNct()`. +``` +this.metaService.setMetaRecord(metaRecord); +``` + +### Trial(jhi-trial) +The main page includes all functions related to trial operations and contains multiple components listed below. +The trial curation is a real time procedure that means users operations on a trial will be broadcast to all sub components, +which is implemented by creating multiple observable objects(watchers) and monitoring them. +All observable objects are created in Trial Service and monitored in constructor methods of referenced components. + +### Arm(jhi-arm) +The arm section stores arm meta info and includes the drug component. + +### Drug(jhi-drug) +The component can be used to store a single drug treatment and combination therapy(drug A + drug B). + +### Panel(jhi-panel) +It contains all node related operations, including add, delete, edit, move, clone and switch. +There is a very important class object called **MovingPath** which contains `from` and `to`. +When a user moves a node, we need to store the source path in `from` and destination path in `to` first, +and then clone, copy and paste the node. + +### Match(jhi-match) +The Match component is a big set of clinical and genomic nodes. They are connected by `AND/OR` logistic symbols. + +### Clinical(jhi-clinical) +The Clinical node contains patient data like `age_numerical`, `oncotree_primary_diagnosis` and `gender`. +Either `age_numerical` and `oncotree_primary_diagnosis` is **required**. + +### Genomic(jhi-genomic) + +The genomic node contains gene data like `hugo_symbol`, `annotated_variant`, `germline` and `matching_examples`. +Both `hugo_symbol` and `annotated_variant` are **required**. + +## Services +### Connection +All API http requests should be put here. + +### Trial +Trial service provides all utility functions related to trials. +All observable objects and update services related to trial should be put here. + +### Email +Email service is a simple service that only has the sending emails function. + +### Meta +All Meta page related utility functions should be put here. + + +## Relationship between components +### Trials Page +![Trials Page](/codedoc/trials.png) + +### Meta Page +![Meta Page](/codedoc/meta.png) diff --git a/codedoc/meta.png b/codedoc/meta.png new file mode 100644 index 00000000..7fa7b452 Binary files /dev/null and b/codedoc/meta.png differ diff --git a/codedoc/trials.png b/codedoc/trials.png new file mode 100644 index 00000000..b30f6f9e Binary files /dev/null and b/codedoc/trials.png differ diff --git a/package-lock.json b/package-lock.json index 0d6ccaac..d81adc8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1223,6 +1223,63 @@ "typescript": "~2.6.2" } }, + "@sentry/browser": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-4.6.6.tgz", + "integrity": "sha512-+9VsQ+oQYU+PYlLJG2ex7JCMSVQbnUvWPI2uZOofWdI9sWIPUub3boWItMzRQNQ1T4S3FZd4FqAWNFd3azdnBw==", + "requires": { + "@sentry/core": "4.6.6", + "@sentry/types": "4.5.3", + "@sentry/utils": "4.6.5", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-4.6.6.tgz", + "integrity": "sha512-7z9HKLTNr3zVBR3tBRheTxkkkuK2IqISUc5Iyo3crN2OecOLtpptT96f5XjLndBEL2ab39eCBPpA5OFjbpzrIA==", + "requires": { + "@sentry/hub": "4.6.5", + "@sentry/minimal": "4.6.5", + "@sentry/types": "4.5.3", + "@sentry/utils": "4.6.5", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-4.6.5.tgz", + "integrity": "sha512-v9vee8s8C1fK/DPtNOzv6r+AMbPDOWfnasouNcBUkbQUSN5wUNyCDvt51QbWaw5kMMY5TSqjdVqY6gXQZI0APQ==", + "requires": { + "@sentry/types": "4.5.3", + "@sentry/utils": "4.6.5", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-4.6.5.tgz", + "integrity": "sha512-tf+J+uUNmSgzC7d9JSN8Ekw1HeBAX87Efa/jyFbzLvaw80oibvTiLSLqDjQ9PgvyIzBUuuPImkS2NpvPQGWFtg==", + "requires": { + "@sentry/hub": "4.6.5", + "@sentry/types": "4.5.3", + "tslib": "^1.9.3" + } + }, + "@sentry/types": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-4.5.3.tgz", + "integrity": "sha512-7ll1PAFNjrBNX9rzy3P2qAQrpQwHaDO3uKj735qsnGw34OtAS8Xr8WYrjI14f9fMPa/XIeWvMPb4GMic28V/ag==" + }, + "@sentry/utils": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-4.6.5.tgz", + "integrity": "sha512-rTISJtRRbWsd3UE+TkA3QG1C0VzPKPW8w74tieBwYhtTCGmOHNwz2nDC/MZGbGj4OgDmNKKl4CCyQr88EX08hA==", + "requires": { + "@sentry/types": "4.5.3", + "tslib": "^1.9.3" + } + }, "@swimlane/ngx-datatable": { "version": "11.3.2", "resolved": "https://registry.npmjs.org/@swimlane/ngx-datatable/-/ngx-datatable-11.3.2.tgz", @@ -2152,6 +2209,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", "dev": true, + "optional": true, "requires": { "hoek": "2.x.x" } @@ -2492,7 +2550,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2513,12 +2572,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2533,17 +2594,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2660,7 +2724,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2672,6 +2737,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2686,6 +2752,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2693,12 +2760,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2717,6 +2786,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2797,7 +2867,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2809,6 +2880,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2894,7 +2966,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -2930,6 +3003,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -2949,6 +3023,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -2992,12 +3067,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -6710,7 +6787,8 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "bundled": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6728,11 +6806,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "bundled": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6745,15 +6825,18 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "concat-map": { "version": "0.0.1", - "bundled": true + "bundled": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "bundled": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6856,7 +6939,8 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "bundled": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6866,6 +6950,7 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6878,17 +6963,20 @@ "minimatch": { "version": "3.0.4", "bundled": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "bundled": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6905,6 +6993,7 @@ "mkdirp": { "version": "0.5.1", "bundled": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6977,7 +7066,8 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "bundled": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6987,6 +7077,7 @@ "once": { "version": "1.4.0", "bundled": true, + "optional": true, "requires": { "wrappy": "1" } @@ -7062,7 +7153,8 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -7092,6 +7184,7 @@ "string-width": { "version": "1.0.2", "bundled": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -7109,6 +7202,7 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -7147,11 +7241,13 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true + "bundled": true, + "optional": true }, "yallist": { "version": "3.0.3", - "bundled": true + "bundled": true, + "optional": true } } }, @@ -8554,7 +8650,8 @@ "version": "2.16.3", "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true + "dev": true, + "optional": true }, "hosted-git-info": { "version": "2.5.0", diff --git a/package.json b/package.json index d5a41898..fe3c130a 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,8 @@ "xml2js": "0.4.17" }, "engines": { - "node": ">=6.9.0" + "node": "8.9.4", + "yarn": "1.3.2" }, "resolutions": { "source-map": "0.6.1" diff --git a/src/main/webapp/app/app.module.ts b/src/main/webapp/app/app.module.ts index fc618c82..98d4aa63 100644 --- a/src/main/webapp/app/app.module.ts +++ b/src/main/webapp/app/app.module.ts @@ -45,7 +45,6 @@ import { import { EmailService } from './service/email.service'; import { ConverterComponent } from './converter/converter.component'; import { ConnectionService } from './service/connection.service'; -import { MainutilService } from './service/mainutil.service'; import { DrugComponent } from './drug/drug.component'; import { MetaComponent } from './meta/meta.component'; import { MetaService } from './service/meta.service'; @@ -55,7 +54,8 @@ import { NgxDatatableModule } from '@swimlane/ngx-datatable'; import * as Sentry from '@sentry/browser'; Sentry.init({ - dsn: 'https://73c005cfa16b49b9825b0ae57b7b9234@sentry.io/1423208' + dsn: environment.sentry_dsn, + blacklistUrls: [new RegExp('.*localhost.*')] }); @Injectable() @@ -109,7 +109,6 @@ export class SentryErrorHandler implements ErrorHandler { TrialService, ConnectionService, EmailService, - MainutilService, MetaService, UserRouteAccessService, { diff --git a/src/main/webapp/app/arm/arm.component.html b/src/main/webapp/app/arm/arm.component.html index b43e911b..f45de772 100644 --- a/src/main/webapp/app/arm/arm.component.html +++ b/src/main/webapp/app/arm/arm.component.html @@ -1,7 +1,7 @@ -arm code: -
+
- +
-arm description:
@@ -9,7 +9,7 @@
-arm internal id:
- +
-arm suspended:
@@ -40,9 +40,11 @@
- -drugs: + -drugs:
- +
+ +
@@ -62,7 +64,11 @@ -arm eligibility:
{{unit.arm_eligibility}}
-drugs: -
{{displayDrugName(unit.drugs)}}
+
+
+ - {{displayDrugName(drugGroup)}} +
+
diff --git a/src/main/webapp/app/arm/arm.component.ts b/src/main/webapp/app/arm/arm.component.ts index 462153e4..95a792ce 100644 --- a/src/main/webapp/app/arm/arm.component.ts +++ b/src/main/webapp/app/arm/arm.component.ts @@ -2,7 +2,8 @@ import { Component, OnInit, Input } from '@angular/core'; import { TrialService } from '../service/trial.service'; import { Arm } from '../arm/arm.model'; import { Drug } from '../drug/drug.model'; -import { MainutilService } from '../service/mainutil.service'; +import MainUtil from '../service/mainutil'; + @Component({ selector: 'jhi-arm', templateUrl: './arm.component.html', @@ -14,11 +15,9 @@ export class ArmComponent implements OnInit { @Input() path = ''; operationPool: {}; armInput: Arm; - oncokb: boolean; + oncokb = MainUtil.oncokb; - constructor(private trialService: TrialService, public mainutilService: MainutilService) { - this.oncokb = this.trialService.oncokb; - } + constructor(private trialService: TrialService) {} ngOnInit() { this.trialService.operationPoolObs.subscribe((message) => { @@ -29,11 +28,12 @@ export class ArmComponent implements OnInit { }); } unCheckRadio(key, event) { - this.armInput[key] = this.mainutilService.unCheckRadio(this.armInput[key], event.target.value); + this.armInput[key] = MainUtil.uncheckRadio(this.armInput[key], event.target.value); + } + displayDrugName(drugGroup: Drug[]) { + return drugGroup.map((drug) => drug.name).join(' + '); } - displayDrugName(drugs: Array) { - if (drugs && drugs.length > 0) { - return drugs.map( (drug) => drug.name).join(', '); - } + addDrugGroup() { + this.armInput.drugs.push([]); } } diff --git a/src/main/webapp/app/arm/arm.model.ts b/src/main/webapp/app/arm/arm.model.ts index 6b51db7d..1753f686 100644 --- a/src/main/webapp/app/arm/arm.model.ts +++ b/src/main/webapp/app/arm/arm.model.ts @@ -1,7 +1,7 @@ import { Drug } from "../drug/drug.model"; export interface Arm { - arm_code: string, // The 1st word of arm_description. + arm_code?: string, // The 1st word of arm_description. arm_description: string, // Arm full name. arm_name?: string, arm_internal_id?: string, // Used for matchminer backend. @@ -9,7 +9,7 @@ export interface Arm { arm_type?: string, // Arm type(Control Arm) arm_eligibility?: string, // Store in Firebase and do not send to MongoDB. arm_info?: string, // Real arm description. Store in Firebase and do not send to MongoDB. - drugs?: Array, - match: Array + drugs?: Drug[][], + match?: object[] } diff --git a/src/main/webapp/app/arm/arm.scss b/src/main/webapp/app/arm/arm.scss index 107433f8..716a47b0 100644 --- a/src/main/webapp/app/arm/arm.scss +++ b/src/main/webapp/app/arm/arm.scss @@ -1,6 +1,3 @@ -input[type="radio"], input[type="checkbox"] { - margin-right: 5px; -} .indent1 { margin-left: 40px; } @@ -15,3 +12,4 @@ input[type="radio"], input[type="checkbox"] { .label-margin { margin-right:10px; } + diff --git a/src/main/webapp/app/clinical/clinical.component.html b/src/main/webapp/app/clinical/clinical.component.html index 3e21456d..c2d7bd23 100644 --- a/src/main/webapp/app/clinical/clinical.component.html +++ b/src/main/webapp/app/clinical/clinical.component.html @@ -6,7 +6,7 @@ {{ validationMessage }}
- +
@@ -53,18 +53,17 @@ {{ validationMessage }}
-
-
- +
+
-
oncotree_primary_diagnosis:
-
+
oncotree_primary_diagnosis:
+
-
+
diff --git a/src/main/webapp/app/clinical/clinical.component.ts b/src/main/webapp/app/clinical/clinical.component.ts index 4d6d2e7d..14f7c1ac 100644 --- a/src/main/webapp/app/clinical/clinical.component.ts +++ b/src/main/webapp/app/clinical/clinical.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, Input } from '@angular/core'; import { TrialService } from '../service/trial.service'; import { Clinical } from './clinical.model'; import * as _ from 'lodash'; -import { MainutilService } from '../service/mainutil.service'; +import MainUtil from '../service/mainutil'; @Component({ selector: 'jhi-clinical', @@ -22,7 +22,7 @@ export class ClinicalComponent implements OnInit { subToMainMapping = this.trialService.getSubToMainMapping(); mainTypesOptions = this.trialService.getMainTypesOptions(); - constructor(private trialService: TrialService, public mainutilService: MainutilService) { } + constructor(private trialService: TrialService) { } ngOnInit() { this.trialService.clinicalInputObs.subscribe((message) => { @@ -33,7 +33,7 @@ export class ClinicalComponent implements OnInit { }); } getStyle() { - return this.trialService.getStyle(this.indent); + return MainUtil.getStyle(this.indent); } getMessageStyle() { if (this.validation === true) { @@ -48,7 +48,7 @@ export class ClinicalComponent implements OnInit { const ageGroups = this.clinicalInput.age_numerical.split(','); // Age input cannot only accepts age range groups greater than 2. if (ageGroups.length === 2) { - const ageNumber = this.clinicalInput.age_numerical.match(/\d+(\.?\d+)?/g).map(function(v) { return Number(v); }); + const ageNumber = this.clinicalInput.age_numerical.match(/\d+(\.?\d+)?/g).map((v: string) => Number(v)); // Do no allow age range like '>15, >=30' or '<=60, <40' or '<10, >60' or '>50, <20' if ((ageGroups[0].includes('>') && ageGroups[1].includes('>')) || (ageGroups[0].includes('<') && ageGroups[1].includes('<')) || @@ -119,6 +119,6 @@ export class ClinicalComponent implements OnInit { return this.trialService.getNodeDisplayContent(key, this.unit['clinical']); } unCheckRadio(key, event) { - this.clinicalInput[key] = this.mainutilService.unCheckRadio(this.clinicalInput[key], event.target.value); + this.clinicalInput[key] = MainUtil.uncheckRadio(this.clinicalInput[key], event.target.value); } } diff --git a/src/main/webapp/app/clinical/clinical.scss b/src/main/webapp/app/clinical/clinical.scss index fb5198a9..e02327b0 100644 --- a/src/main/webapp/app/clinical/clinical.scss +++ b/src/main/webapp/app/clinical/clinical.scss @@ -19,9 +19,6 @@ label { width:300px; } } -.editMargin{ - margin-left:-80px; -} .ageNumericalMargin{ margin-bottom:10px } diff --git a/src/main/webapp/app/drug/drug.component.html b/src/main/webapp/app/drug/drug.component.html index a766a3be..eebc6a8b 100644 --- a/src/main/webapp/app/drug/drug.component.html +++ b/src/main/webapp/app/drug/drug.component.html @@ -1,11 +1,17 @@ - - -
- {{item.name}} - {{'NCIT Code: ' + item.ncit_code}} -
-
{{'Also known as ' + item.synonyms}}
-
-
+ +
+ + +
+ {{item.name}} + {{'NCIT Code: ' + item.ncit_code}} +
+
{{'Also known as ' + item.synonyms}}
+
+
+
+ +
+ diff --git a/src/main/webapp/app/drug/drug.component.ts b/src/main/webapp/app/drug/drug.component.ts index 27cab020..2416631d 100644 --- a/src/main/webapp/app/drug/drug.component.ts +++ b/src/main/webapp/app/drug/drug.component.ts @@ -2,6 +2,8 @@ import { Component, EventEmitter, Input, OnInit } from '@angular/core'; import { TrialService } from '../service/trial.service'; import { Drug, NcitDrug } from '../drug/drug.model'; import { debounceTime, switchMap } from 'rxjs/operators'; +import { Arm } from '../arm/arm.model'; +import MainUtil from '../service/mainutil'; @Component({ selector: 'jhi-drug', @@ -10,11 +12,11 @@ import { debounceTime, switchMap } from 'rxjs/operators'; }) export class DrugComponent implements OnInit { - @Input() armInput = {}; + @Input() armInput: Arm = MainUtil.createArm(); + @Input() drugGroupIndex = 0; drugsOptionsLoading = false; - selectedDrugs: Array = []; drugInput = new EventEmitter(); - drugsOptions: Array = []; + drugsOptions: Drug[] = []; constructor(private trialService: TrialService) { this.drugInput.pipe( @@ -40,13 +42,12 @@ export class DrugComponent implements OnInit { } ngOnInit() { - this.trialService.armInputObs.subscribe((message) => { + this.trialService.armInputObs.subscribe((message: Arm) => { this.armInput = message; - this.selectedDrugs = this.armInput['drugs']; }); } - saveDrugs() { - this.armInput['drugs'] = this.selectedDrugs.map((drug: any) => typeof drug === 'string' ? { 'name': drug } : drug); + removeDrugGroup() { + this.armInput.drugs.splice(this.drugGroupIndex, 1); } } diff --git a/src/main/webapp/app/drug/drug.scss b/src/main/webapp/app/drug/drug.scss index 9e12db05..1c8187f7 100644 --- a/src/main/webapp/app/drug/drug.scss +++ b/src/main/webapp/app/drug/drug.scss @@ -1,6 +1,3 @@ -.drug-select { - max-width: 813px; -} .drug-name { color: black; font-size: 15px; diff --git a/src/main/webapp/app/environments/environment.example.ts b/src/main/webapp/app/environments/environment.example.ts index 558c1c68..e8d2dc65 100644 --- a/src/main/webapp/app/environments/environment.example.ts +++ b/src/main/webapp/app/environments/environment.example.ts @@ -11,4 +11,5 @@ export const environment = { oncotreeVersion: '', // set for using specific oncotree version. By default, it is "oncotree_latest_stable". frontEndOnly: true, // set to true if doing frontend development isPermitted: true, // set to false for building read-only website + sentry_dsn: '' // set Sentry dsn for tracking issues }; diff --git a/src/main/webapp/app/genomic/genomic.component.html b/src/main/webapp/app/genomic/genomic.component.html index dfe263da..0a477147 100644 --- a/src/main/webapp/app/genomic/genomic.component.html +++ b/src/main/webapp/app/genomic/genomic.component.html @@ -9,7 +9,7 @@
- +
@@ -19,7 +19,7 @@
- +
@@ -27,7 +27,7 @@ {{ validationMessage['example'] }}
- +
@@ -35,10 +35,10 @@
@@ -174,7 +174,7 @@
- +
@@ -184,7 +184,7 @@
- +
@@ -192,7 +192,7 @@ {{ validationMessage['example'] }}
- +
@@ -200,10 +200,10 @@
diff --git a/src/main/webapp/app/genomic/genomic.component.ts b/src/main/webapp/app/genomic/genomic.component.ts index 47e91673..caf14ec5 100644 --- a/src/main/webapp/app/genomic/genomic.component.ts +++ b/src/main/webapp/app/genomic/genomic.component.ts @@ -7,7 +7,7 @@ import 'rxjs/add/operator/distinctUntilChanged'; import { Genomic } from './genomic.model'; import * as _ from 'lodash'; import { ConnectionService } from '../service/connection.service'; -import { MainutilService } from '../service/mainutil.service'; +import MainUtil from '../service/mainutil'; @Component({ selector: 'jhi-genomic', @@ -31,7 +31,7 @@ export class GenomicComponent implements OnInit { 'protein_altering', 'splice site_mutation', 'stop_retained', 'synonymous', '3\'UTR', '3_prime_UTR', '5\'Flank', '5\'UTR', '5\'UTR_mutation', '5_prime_UTR']; annotated_variants = this.trialService.getOncokbVariants(); - oncokb = this.trialService.oncokb; + oncokb = MainUtil.oncokb; validationMessage = { gene: '', example: '' @@ -56,8 +56,8 @@ export class GenomicComponent implements OnInit { } } - constructor(private trialService: TrialService, public connectionService: ConnectionService, public mainutilService: MainutilService) { - } + constructor(private trialService: TrialService, public connectionService: ConnectionService) {} + ngOnInit() { this.trialService.genomicInputObs.subscribe((message) => { this.genomicInput = message; @@ -67,7 +67,7 @@ export class GenomicComponent implements OnInit { }); } getStyle() { - return this.trialService.getStyle(this.indent); + return MainUtil.getStyle(this.indent); } // This validation function will be executed the moment the input box lose focus validateGenomicGene() { @@ -114,6 +114,6 @@ export class GenomicComponent implements OnInit { return this.trialService.getNodeDisplayContent(key, this.unit['genomic']); } unCheckRadio(key, event) { - this.genomicInput[key] = this.mainutilService.unCheckRadio(this.genomicInput[key], event.target.value); + this.genomicInput[key] = MainUtil.uncheckRadio(this.genomicInput[key], event.target.value); } } diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.ts b/src/main/webapp/app/layouts/navbar/navbar.component.ts index 60c1e8fd..ac075c28 100644 --- a/src/main/webapp/app/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/layouts/navbar/navbar.component.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { Principal, LoginModalService, LoginService } from '../../shared'; import { VERSION } from '../../app.constants'; -import { environment } from '../../environments/environment'; +import MainUtil from '../../service/mainutil'; @Component({ selector: 'jhi-navbar', @@ -29,7 +29,7 @@ export class NavbarComponent implements OnInit { ) { this.version = VERSION ? 'v' + VERSION : ''; this.isNavbarCollapsed = true; - this.oncokb = environment['oncokb'] ? environment['oncokb'] : false; + this.oncokb = MainUtil.oncokb; } ngOnInit() {} diff --git a/src/main/webapp/app/login/login.component.ts b/src/main/webapp/app/login/login.component.ts index 0fa37e2b..e2082ad4 100644 --- a/src/main/webapp/app/login/login.component.ts +++ b/src/main/webapp/app/login/login.component.ts @@ -3,16 +3,19 @@ import * as firebase from 'firebase/app'; import { Observable } from 'rxjs/Observable'; import { TrialService } from '../service/trial.service'; import { AngularFireAuth } from '@angular/fire/auth'; -import { environment } from '../environments/environment'; import { MetaService } from '../service/meta.service'; +import MainUtil from '../service/mainutil'; + @Component({ selector: 'jhi-login', templateUrl: './login.component.html', styleUrls: [ 'login.scss' ] }) + export class LoginComponent { public user: Observable; - oncokb = environment['oncokb'] ? environment['oncokb'] : false; + oncokb = MainUtil.oncokb; + constructor(public afAuth: AngularFireAuth, private trialService: TrialService, private metaService: MetaService) { this.user = this.afAuth.authState; this.user.subscribe((res) => { diff --git a/src/main/webapp/app/match/match.component.ts b/src/main/webapp/app/match/match.component.ts index efeaa70b..61de2f88 100644 --- a/src/main/webapp/app/match/match.component.ts +++ b/src/main/webapp/app/match/match.component.ts @@ -1,23 +1,26 @@ import { Component, OnInit, Input } from '@angular/core'; -import { TrialService } from '../service/trial.service'; import * as _ from 'lodash'; +import MainUtil from '../service/mainutil'; + @Component({ selector: 'jhi-match', templateUrl: './match.component.html', styleUrls: ['match.scss'] }) + export class MatchComponent implements OnInit { @Input() match: Array; @Input() base = 0; @Input() path = ''; - constructor(private trialService: TrialService) { - } - ngOnInit() { - } + constructor() {} + + ngOnInit() {} + getStyle(indent: number) { - return this.trialService.getStyle(this.base + indent); + return MainUtil.getStyle(this.base + indent); } + isValidMatch(unit: object) { return !_.isUndefined(unit); } diff --git a/src/main/webapp/app/meta/meta.component.ts b/src/main/webapp/app/meta/meta.component.ts index d2d7eff7..4e37bc70 100644 --- a/src/main/webapp/app/meta/meta.component.ts +++ b/src/main/webapp/app/meta/meta.component.ts @@ -2,9 +2,8 @@ import { Component, ViewChild } from '@angular/core'; import { TrialService } from '../service/trial.service'; import * as _ from 'lodash'; import { Meta } from './meta.model'; -import { MainutilService } from '../service/mainutil.service'; +import MainUtil from '../service/mainutil'; import { MetaService } from '../service/meta.service'; -import { environment } from '../environments/environment'; import { DatatableComponent } from '@swimlane/ngx-datatable'; import { saveAs } from 'file-saver'; @@ -15,14 +14,14 @@ import { saveAs } from 'file-saver'; }) export class MetaComponent { - oncokb = environment['oncokb'] ? environment['oncokb'] : false; + oncokb = MainUtil.oncokb; @ViewChild(DatatableComponent) table: DatatableComponent; loadingIndicator = true; rows = []; temp = []; statusOptions = this.trialService.statusOptions; - constructor(private trialService: TrialService, public mainutilService: MainutilService, public metaService: MetaService) { + constructor(private trialService: TrialService, public metaService: MetaService) { this.metaService.metaListObs.subscribe( ( result ) => { if (result.length > 0) { this.rows = [...result]; @@ -37,7 +36,7 @@ export class MetaComponent { updateValue(key: string, event: any, data: Meta, rowIndex) { if (key === 'precision_medicine') { const originalData = _.clone(data); - data[key] = this.mainutilService.unCheckRadio(data[key], event.target.value); + data[key] = MainUtil.uncheckRadio(data[key], event.target.value); if (originalData[key] !== data[key]) { this.metaService.updateMetaRecord(key, data); this.rows[rowIndex][key] = data[key]; diff --git a/src/main/webapp/app/panel/panel.component.html b/src/main/webapp/app/panel/panel.component.html index fd1ce23a..1fe1c805 100644 --- a/src/main/webapp/app/panel/panel.component.html +++ b/src/main/webapp/app/panel/panel.component.html @@ -12,7 +12,7 @@ -
+

- +
@@ -69,7 +69,7 @@
Clinical:
arm internal id:
- +
@@ -116,10 +116,27 @@
Clinical:
- + drugs:
- +
+ +
+ + +
+ {{item.name}} + {{'NCIT Code: ' + item.ncit_code}} +
+
{{'Also known as ' + item.synonyms}}
+
+
+
+ +
+
diff --git a/src/main/webapp/app/panel/panel.component.ts b/src/main/webapp/app/panel/panel.component.ts index 811b634d..4a8c2862 100644 --- a/src/main/webapp/app/panel/panel.component.ts +++ b/src/main/webapp/app/panel/panel.component.ts @@ -1,15 +1,20 @@ -import { Component, OnInit, Input } from '@angular/core'; +import { Component, OnInit, Input, EventEmitter } from '@angular/core'; import { TrialService } from '../service/trial.service'; import * as _ from 'lodash'; import { Genomic } from '../genomic/genomic.model'; import { Clinical } from '../clinical/clinical.model'; import { MovingPath } from './movingPath.model'; import { Arm } from '../arm/arm.model'; +import MainUtil from '../service/mainutil'; +import { Drug, NcitDrug } from '../drug/drug.model'; +import { debounceTime, switchMap } from 'rxjs/operators'; + @Component({ selector: 'jhi-panel', templateUrl: './panel.component.html', styleUrls: ['panel.scss'] }) + export class PanelComponent implements OnInit { @Input() path = ''; // used to manage the icons to be displayed @@ -31,13 +36,15 @@ export class PanelComponent implements OnInit { armInput: Arm; originalMatch = []; originalArms = []; + originalArmDrug: Drug[][]; dataToModify = []; allSubTypesOptions = this.trialService.getAllSubTypesOptions(); subToMainMapping = this.trialService.getSubToMainMapping(); mainTypesOptions = this.trialService.getMainTypesOptions(); statusOptions = this.trialService.getStatusOptions(); nctIdChosen: string; - isPermitted = this.trialService.isPermitted; + oncokb = MainUtil.oncokb; + isPermitted = MainUtil.isPermitted; trialChosen: {}; genomicInput: Genomic; clinicalInput: Clinical; @@ -45,11 +52,33 @@ export class PanelComponent implements OnInit { genomicFields = ['hugo_symbol', 'annotated_variant', 'matching_examples', 'germline', 'protein_change', 'wildcard_protein_change', 'variant_classification', 'variant_category', 'exon', 'cnv_call', 'wildtype', 'ms_status', 'mmr_status']; oncokbGenomicFields = ['hugo_symbol', 'annotated_variant', 'germline']; - oncokb: boolean; hasErrorInputField: boolean; copyMatch = false; + drugInput = new EventEmitter(); + drugsOptions: Drug[] = []; + drugsOptionsLoading = false; constructor(private trialService: TrialService) { + this.drugInput.pipe( + debounceTime(200), + switchMap((term) => { + this.drugsOptionsLoading = true; + return this.trialService.loadDrugsOptions(term); + }) + ).subscribe((items) => { + this.drugsOptions = items.map((drug: NcitDrug) => { + const drugOption: Drug = { + ncit_code: drug.codes.join(', '), + name: drug.name, + synonyms: drug.synonyms.join(', ') + }; + return drugOption; + }); + this.drugsOptionsLoading = false; + }, (err) => { + this.drugsOptions = []; + this.drugsOptionsLoading = false; + }); } ngOnInit() { @@ -80,7 +109,6 @@ export class PanelComponent implements OnInit { this.trialService.hasErrorInputFieldObs.subscribe((message) => { this.hasErrorInputField = message; }); - this.oncokb = this.trialService.oncokb; } preparePath(pathParameter?: string) { const pathStr = pathParameter ? pathParameter : this.path; @@ -139,7 +167,7 @@ export class PanelComponent implements OnInit { } if (result && (type === 'delete' || !hasEmptySections)) { - if (this.arm === true) { + if (this.arm) { this.modifyArmGroup(type); } else { this.preparePath(); @@ -321,24 +349,17 @@ export class PanelComponent implements OnInit { this.clearNodeInput(); } clearNodeInput() { - this.trialService.setGenomicInput(this.trialService.createGenomic()); - this.trialService.setClinicalInput(this.trialService.createClinical()); + this.trialService.setGenomicInput(MainUtil.createGenomic()); + this.trialService.setClinicalInput(MainUtil.createClinical()); + this.trialService.setArmInput(MainUtil.createArm()); } - clearInputForm(keys: Array, type: string) { + clearInputForm(type: string) { if (type === 'Genomic') { - for (const key of keys) { - this.genomicInput[key] = ''; - this.genomicInput['no_' + key] = false; - } + this.genomicInput = MainUtil.createGenomic(); } else if (type === 'Clinical') { - for (const key of keys) { - this.clinicalInput[key] = ''; - this.clinicalInput['no_' + key] = false; - } - } else if (type === 'arm') { - for (const key of keys) { - this.armInput[key] = ''; - } + this.clinicalInput = MainUtil.createClinical(); + } else if (type === 'Arm') { + this.armInput = MainUtil.createArm(); } } getOncotree() { @@ -521,6 +542,7 @@ export class PanelComponent implements OnInit { drugs: this.unit['drugs'], match: this.unit['match'] }; + this.originalArmDrug = _.cloneDeep(this.unit['drugs']); this.trialService.setArmInput(armToAdd); } } @@ -563,17 +585,12 @@ export class PanelComponent implements OnInit { } preAddNode() { this.addNode = true; - if (this.arm === true) { - if (this.trialService.oncokb) { - this.clearInputForm(['arm_code', 'arm_description', 'arm_internal_id', 'arm_suspended', 'match', 'arm_type', - 'arm_info', 'arm_eligibility', 'drugs'], 'arm'); - } else { - this.clearInputForm(['arm_code', 'arm_description', 'arm_internal_id', 'arm_suspended', 'match'], 'arm'); - } + if (this.arm) { + this.clearInputForm('Arm'); } } moveNode() { - if (this.operationPool['relocate'] === true) { + if (this.operationPool['relocate']) { this.operationPool['currentPath'] = ''; this.operationPool['relocate'] = false; } else { @@ -583,7 +600,7 @@ export class PanelComponent implements OnInit { } } copyNode() { - if (this.operationPool['copy'] === true) { + if (this.operationPool['copy']) { // click twice for canceling copy operation this.operationPool['currentPath'] = ''; this.operationPool['copy'] = false; @@ -594,6 +611,10 @@ export class PanelComponent implements OnInit { } } cancelModification() { + if (this.arm) { + this.armInput.drugs = this.originalArmDrug; + this.unit['drugs'] = this.originalArmDrug; + } this.operationPool['currentPath'] = ''; this.operationPool['editing'] = false; } @@ -642,7 +663,7 @@ export class PanelComponent implements OnInit { removeOriginalNode(match: Array) { const itemsToRemove = []; for (const item of match) { - if (item.toBeRemoved === true) { + if (item.toBeRemoved) { itemsToRemove.push(item); } } @@ -759,17 +780,7 @@ export class PanelComponent implements OnInit { modifyArmGroup(type, arm?: Arm) { if (type === 'add') { if (_.isUndefined(arm)) { - const armToAdd: Arm = { - arm_code: '', - arm_description: '', - arm_internal_id: '', - arm_suspended: '', - arm_type: '', - arm_eligibility: '', - arm_info: '', - drugs: [], - match: [] - }; + const armToAdd: Arm = MainUtil.createArm(); this.prepareArmData(this.armInput, armToAdd); arm = armToAdd; } @@ -783,6 +794,15 @@ export class PanelComponent implements OnInit { } } prepareArmData(armInput: Arm, armToSave: Arm) { + this.armInput.drugs = this.armInput.drugs.map((drugGroup: any[]) => drugGroup.map((drug: Drug| string) => { + if (typeof drug === 'string') { + const newDrug: Drug = { + name: drug, + }; + return newDrug; + } + return drug; + })); const keys = _.keys(armInput); _.forEach(keys, function(key) { if (!_.isUndefined(armInput[key])) { @@ -805,4 +825,10 @@ export class PanelComponent implements OnInit { this.armInput.arm_type = ''; } } + addDrugGroup() { + this.armInput.drugs.push([]); + } + removeDrugGroup(index: number) { + this.armInput.drugs.splice(index, 1); + } } diff --git a/src/main/webapp/app/panel/panel.scss b/src/main/webapp/app/panel/panel.scss index fa4bd1fb..304eaab0 100644 --- a/src/main/webapp/app/panel/panel.scss +++ b/src/main/webapp/app/panel/panel.scss @@ -13,34 +13,7 @@ input[type="radio"], input[type="checkbox"] { font-weight: bold; padding: 4px } -.addIcon { - color: green; - margin-right:8px -} -.deleteIcon { - color: red; - margin-right:8px -} -.editIcon { - margin-right:8px -} -.exchangeIcon { - color: green; - margin-right:8px -} -.moveIcon { - font-size:14px; - color: orange; - margin-right:8px -} -.copyIcon { - color: #3E8ACC; - margin-right:8px -} -.destinationIcon { - color: green; - margin-right:8px -} + .addButon { padding:5px; margin:5px diff --git a/src/main/webapp/app/service/connection.service.ts b/src/main/webapp/app/service/connection.service.ts index f7fc3ff9..cea38fb0 100644 --- a/src/main/webapp/app/service/connection.service.ts +++ b/src/main/webapp/app/service/connection.service.ts @@ -1,13 +1,11 @@ import { Injectable } from '@angular/core'; -import { environment } from '../environments/environment'; import { SERVER_API_URL } from '../app.constants'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; +import MainUtil from './mainutil'; @Injectable() export class ConnectionService { - frontEndOnly = environment.frontEndOnly ? environment.frontEndOnly : false; - oncotreeVersion = environment.oncotreeVersion ? environment.oncotreeVersion : 'oncotree_latest_stable'; constructor(private http: HttpClient) { } @@ -15,10 +13,10 @@ export class ConnectionService { if (type === 'Drugs') { return 'https://clinicaltrialsapi.cancer.gov/v1/interventions'; } - if (this.frontEndOnly) { + if (MainUtil.frontEndOnly) { switch (type) { case 'MainType': - return 'http://oncotree.mskcc.org/api/mainTypes?version=' + this.oncotreeVersion; + return 'http://oncotree.mskcc.org/api/mainTypes?version=' + MainUtil.oncotreeVersion; case 'SubType': return 'http://oncotree.mskcc.org/api/tumorTypes/search'; case 'OncoKBVariant': @@ -35,7 +33,7 @@ export class ConnectionService { } else { switch (type) { case 'MainType': - return SERVER_API_URL + 'proxy/http/oncotree.mskcc.org/api/mainTypes?version=' + this.oncotreeVersion; + return SERVER_API_URL + 'proxy/http/oncotree.mskcc.org/api/mainTypes?version=' + MainUtil.oncotreeVersion; case 'SubType': return SERVER_API_URL + 'proxy/http/oncotree.mskcc.org/api/tumorTypes/search'; case 'OncoKBVariant': diff --git a/src/main/webapp/app/service/mainutil.service.ts b/src/main/webapp/app/service/mainutil.service.ts deleted file mode 100644 index 0a655e52..00000000 --- a/src/main/webapp/app/service/mainutil.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class MainutilService { - - constructor() { } - - unCheckRadio(input, value) { - if (value === input) { - value = ''; - } - return value; - } -} diff --git a/src/main/webapp/app/service/mainutil.ts b/src/main/webapp/app/service/mainutil.ts new file mode 100644 index 00000000..32037471 --- /dev/null +++ b/src/main/webapp/app/service/mainutil.ts @@ -0,0 +1,124 @@ +import * as _ from 'lodash'; +import { Genomic } from '../genomic/genomic.model'; +import { Clinical } from '../clinical/clinical.model'; +import { Additional, Trial } from '../trial/trial.model'; +import { environment } from '../environments/environment'; +import { Arm } from '../arm/arm.model'; +import { Drug } from '../drug/drug.model'; + +export default class MainUtil { + static oncokb: boolean = environment['oncokb'] ? environment['oncokb'] : false; + static oncotreeVersion: string = environment.oncotreeVersion ? environment.oncotreeVersion : 'oncotree_latest_stable'; + static isPermitted: boolean = environment.isPermitted ? environment.isPermitted : false; + static frontEndOnly = environment.frontEndOnly ? environment.frontEndOnly : false; + static devEmail = environment.devEmail; + + static uncheckRadio(input: string, value: string) { + if (value === input) { + value = ''; + } + return value; + } + static normalizeText(content: string) { + if (MainUtil.isAllUpperCase(content)) { + return content.split(' ').map((str) => _.capitalize(str)).join(' '); + } + return content; + } + static isAllUpperCase(str: string) { + return str.toUpperCase() === str; + } + static updateTimestampByToday() { + return new Date().getTime(); + } + static createGenomic(): Genomic { + let genomicInput: Genomic; + if (this.oncokb) { + genomicInput = { + hugo_symbol: '', + annotated_variant: '', + matching_examples: '', + germline: '', + no_hugo_symbol: false, + no_annotated_variant: false, + }; + } else { + genomicInput = { + hugo_symbol: '', + annotated_variant: '', + matching_examples: '', + germline: '', + protein_change: '', + wildcard_protein_change: '', + variant_classification: '', + variant_category: '', + exon: '', + cnv_call: '', + wildtype: '', + no_hugo_symbol: false, + no_annotated_variant: false, + no_protein_change: false, + no_wildcard_protein_change: false, + no_variant_classification: false, + no_variant_category: false, + no_exon: false, + no_cnv_call: false + }; + } + return genomicInput; + } + static createClinical(): Clinical { + const clinicalInput: Clinical = { + age_numerical: '', + oncotree_primary_diagnosis: '', + main_type: '', + sub_type: '', + no_oncotree_primary_diagnosis: false + }; + return clinicalInput; + } + static createTrial(): Trial { + const trial: Trial = { + curation_status: '', + archived: '', + nct_id: '', + protocol_no: '', + long_title: '', + short_title: '', + phase: '', + status: '', + treatment_list: { step: [] } + }; + return trial; + } + static createAdditional(): Additional { + const additional: Additional = { + note: '' + }; + return additional; + } + static createDrug(): Drug { + const drug: Drug = { + name: '' + }; + return drug; + } + static createArm(): Arm { + const arm: Arm = { + arm_code: '', + arm_description: '', + arm_internal_id: '', + arm_suspended: '', + arm_type: '', + arm_eligibility: '', + arm_info: '', + drugs: [[]], + match: [] + }; + return arm; + } + + static getStyle(indent: number) { + return { 'margin-left': (indent * 40) + 'px' }; + } +} diff --git a/src/main/webapp/app/service/trial.service.ts b/src/main/webapp/app/service/trial.service.ts index b149fbb8..5c54d0ac 100644 --- a/src/main/webapp/app/service/trial.service.ts +++ b/src/main/webapp/app/service/trial.service.ts @@ -6,31 +6,28 @@ import { Clinical } from '../clinical/clinical.model'; import { MovingPath } from '../panel/movingPath.model'; import { Arm } from '../arm/arm.model'; import * as _ from 'lodash'; -import { environment } from '../environments/environment'; import { EmailService } from './email.service'; import { ConnectionService } from './connection.service'; import { HttpErrorResponse } from '@angular/common/http'; import { of } from 'rxjs/observable/of'; import { catchError, map } from 'rxjs/operators'; import { AngularFireDatabase, AngularFireObject } from '@angular/fire/database'; +import MainUtil from './mainutil'; @Injectable() export class TrialService { - oncokb = environment['oncokb'] ? environment['oncokb'] : false; - oncotreeVersion = environment.oncotreeVersion ? environment.oncotreeVersion : 'oncotree_latest_stable'; - isPermitted = environment.isPermitted ? environment.isPermitted : false; private nctIdChosenSource = new BehaviorSubject(''); nctIdChosenObs = this.nctIdChosenSource.asObservable(); - trial = this.createTrial(); + trial = MainUtil.createTrial(); private trialChosenSource = new BehaviorSubject(this.trial); trialChosenObs = this.trialChosenSource.asObservable(); private trialListSource = new BehaviorSubject>([]); trialListObs = this.trialListSource.asObservable(); - additional = this.createAdditional(); + additional = MainUtil.createAdditional(); private additionalChosenSource = new BehaviorSubject(this.additional); additionalChosenObs = this.additionalChosenSource.asObservable(); @@ -53,11 +50,11 @@ export class TrialService { private movingPathSource = new BehaviorSubject(this.movingPath); movingPathObs = this.movingPathSource.asObservable(); - genomicInput = this.createGenomic(); + genomicInput = MainUtil.createGenomic(); private genomicInputSource = new BehaviorSubject(this.genomicInput); genomicInputObs = this.genomicInputSource.asObservable(); - clinicalInput = this.createClinical(); + clinicalInput = MainUtil.createClinical(); private clinicalInputSource = new BehaviorSubject(this.clinicalInput); clinicalInputObs = this.clinicalInputSource.asObservable(); @@ -65,17 +62,7 @@ export class TrialService { private hasErrorInputFieldSource = new BehaviorSubject(this.hasErrorInputField); hasErrorInputFieldObs = this.hasErrorInputFieldSource.asObservable(); - armInput: Arm = { - arm_code: '', - arm_description: '', - arm_internal_id: '', - arm_suspended: '', - arm_type: '', - arm_eligibility: '', - arm_info: '', - drugs: [], - match: [] - }; + armInput = MainUtil.createArm(); private armInputSource = new BehaviorSubject(this.armInput); armInputObs = this.armInputSource.asObservable(); @@ -108,7 +95,7 @@ export class TrialService { for (const item of res) { mainTypeQueries.push({ 'query': item, - 'version': this.oncotreeVersion, + 'version': MainUtil.oncotreeVersion, 'type': 'mainType' }); } @@ -159,72 +146,6 @@ export class TrialService { } }); } - createGenomic() { - let genomicInput: Genomic; - if (this.oncokb === true) { - genomicInput = { - hugo_symbol: '', - annotated_variant: '', - matching_examples: '', - germline: '', - no_hugo_symbol: false, - no_annotated_variant: false, - }; - } else { - genomicInput = { - hugo_symbol: '', - annotated_variant: '', - matching_examples: '', - germline: '', - protein_change: '', - wildcard_protein_change: '', - variant_classification: '', - variant_category: '', - exon: '', - cnv_call: '', - wildtype: '', - no_hugo_symbol: false, - no_annotated_variant: false, - no_protein_change: false, - no_wildcard_protein_change: false, - no_variant_classification: false, - no_variant_category: false, - no_exon: false, - no_cnv_call: false - }; - } - return genomicInput; - } - createClinical() { - const clinicalInput: Clinical = { - age_numerical: '', - oncotree_primary_diagnosis: '', - main_type: '', - sub_type: '', - no_oncotree_primary_diagnosis: false - }; - return clinicalInput; - } - createTrial() { - const trial: Trial = { - curation_status: '', - archived: '', - nct_id: '', - protocol_no: '', - long_title: '', - short_title: '', - phase: '', - status: '', - treatment_list: { step: [] } - }; - return trial; - } - createAdditional() { - const additional: Additional = { - note: '' - }; - return additional; - } fetchTrials() { this.trialsRef.snapshotChanges().subscribe((action) => { this.authorizedSource.next(true); @@ -281,6 +202,9 @@ export class TrialService { if (_.isUndefined(armItem.match)) { armItem.match = []; } + if (_.isUndefined(armItem.drugs)) { + armItem.drugs = [[]]; + } }); } if (_.isUndefined(trial['treatment_list'].step[0].match)) { @@ -309,9 +233,6 @@ export class TrialService { setHasErrorInputField(hasErrorInputField: boolean) { this.hasErrorInputFieldSource.next(hasErrorInputField); } - getStyle(indent: number) { - return { 'margin-left': (indent * 40) + 'px' }; - } getStatusOptions() { return this.statusOptions; } @@ -355,7 +276,7 @@ export class TrialService { saveErrors(info: string, content: object, error: object) { if (info.includes('failed') && info.includes('database')) { this.emailService.sendEmail({ - sendTo: environment.devEmail, + sendTo: MainUtil.devEmail, subject: info, content: 'Content: \n' + JSON.stringify(content) + '\n\n Error: \n' + JSON.stringify(error) }); @@ -386,7 +307,7 @@ export class TrialService { alert('Sorry, required data source is unavailable now.'); } else { this.emailService.sendEmail({ - sendTo: environment.devEmail, + sendTo: MainUtil.devEmail, subject: 'Matchminer Curate http request failed.', content: 'Error: \n' + JSON.stringify(error) }); diff --git a/src/main/webapp/app/trial/trial.component.html b/src/main/webapp/app/trial/trial.component.html index 30dde544..11ce16a1 100644 --- a/src/main/webapp/app/trial/trial.component.html +++ b/src/main/webapp/app/trial/trial.component.html @@ -59,7 +59,7 @@

-
+

@@ -72,7 +72,8 @@ -
+
+
{{ trialChosen.archived }} @@ -81,21 +82,39 @@ -

+
+
+ + {{ trialChosen.protocol_accessed | date: 'yyyy-MM-dd' }} + + {{protocolAccessedMessage.content}} + +
+ +
- + {{protocolNoMessage.content}} -
+
+
+ + + {{ trialChosen.principal_investigator.full_name }} + {{ trialChosen.principal_investigator.full_name }} + +
+
{{ trialChosen.phase }} -
+
+
{{ trialChosen.status }} @@ -104,13 +123,24 @@ [clearable]="false" (change)="updateTrialStatusInDB()">
-
+
+
- {{ trialChosen.short_title }} -
+ + {{ trialChosen.short_title }} + + + + + + + +
+
{{ trialChosen.long_title }} -
+
+
-arm: diff --git a/src/main/webapp/app/trial/trial.component.ts b/src/main/webapp/app/trial/trial.component.ts index 93565359..4ee18c1f 100644 --- a/src/main/webapp/app/trial/trial.component.ts +++ b/src/main/webapp/app/trial/trial.component.ts @@ -3,8 +3,9 @@ import { AngularFireDatabase } from '@angular/fire/database'; import 'rxjs/add/operator/switchMap'; import 'rxjs/add/observable/combineLatest'; import { TrialService } from '../service/trial.service'; +import MainUtil from '../service/mainutil'; import * as _ from 'lodash'; -import { Additional, Trial } from './trial.model'; +import { Additional, Message, Trial } from './trial.model'; import '../../../../../node_modules/jquery/dist/jquery.js'; import '../../../../../node_modules/datatables.net/js/jquery.dataTables.js'; import { Subject } from 'rxjs/Subject'; @@ -14,7 +15,6 @@ import { Router } from '@angular/router'; import { ConnectionService } from '../service/connection.service'; import { MetaService } from '../service/meta.service'; import { Meta } from '../meta/meta.model'; -import { environment } from '../environments/environment'; import { saveAs } from 'file-saver'; @Component( { @@ -24,14 +24,15 @@ import { saveAs } from 'file-saver'; } ) export class TrialComponent implements OnInit, AfterViewInit { - oncokb = environment['oncokb'] ? environment['oncokb'] : false; + oncokb = MainUtil.oncokb; + isPermitted = MainUtil.isPermitted; @ViewChild( DataTableDirective ) dtElement: DataTableDirective; trialsToImport = ''; nctIdChosen = ''; - messages: Array = []; - trialList: Array = []; - trialChosen = {}; + messages: string[] = []; + trialList: Trial[] = []; + trialChosen: Trial; additionalInput: Additional; additionalChosen: Additional; additionalsObject = {}; @@ -40,10 +41,14 @@ export class TrialComponent implements OnInit, AfterViewInit { dtOptions: DataTables.Settings = {}; dtTrigger: Subject = new Subject(); hideArchived = 'Yes'; + displayPencil = true; statusOptions = this.trialService.getStatusOptions(); - originalTrial = {}; - isPermitted = this.trialService.isPermitted; - protocolNoMessage = { + originalTrial: Trial; + protocolNoMessage: Message = { + content: '', + color: '' + }; + protocolAccessedMessage: Message = { content: '', color: '' }; @@ -52,7 +57,10 @@ export class TrialComponent implements OnInit, AfterViewInit { constructor( private trialService: TrialService, private metaService: MetaService, public db: AngularFireDatabase, private connectionService: ConnectionService, private router: Router ) { this.trialService.nctIdChosenObs.subscribe( ( message ) => this.nctIdChosen = message ); - this.trialService.trialChosenObs.subscribe( ( message ) => this.trialChosen = message ); + this.trialService.trialChosenObs.subscribe( ( message ) => { + this.trialChosen = message; + this.originalTrial = _.clone(message); + } ); this.trialService.trialListObs.subscribe( ( message ) => { this.trialList = message; this.trialListIds = this.trialService.trialListIds; @@ -86,7 +94,7 @@ export class TrialComponent implements OnInit, AfterViewInit { ] }; this.nctIdChosen = ''; - this.trialChosen = {}; + this.trialChosen = MainUtil.createTrial(); this.messages = []; if ( this.router.url.includes( 'NCT' ) ) { const urlArray = this.router.url.split( '/' ); @@ -98,39 +106,46 @@ export class TrialComponent implements OnInit, AfterViewInit { if (this.trialListIds.includes(nctId)) { this.curateTrial( nctId ); } else { - this.importTrialsFromNct(nctId, protocolNo); + this.importTrialsFromNct(nctId, {protocol_no : protocolNo}); } } } importTrials() { this.messages = []; - this.protocolNoMessage.content = ''; + this.clearMessages(); const newTrials: Array = this.trialsToImport.split( ',' ); let nctId = ''; - let protocolNo = ''; for ( const newTrial of newTrials ) { const tempTrial = newTrial.trim(); if ( tempTrial.length === 0 ) { continue; - } else if ( tempTrial.match( /NCT[0-9]+/g ) ) { + } else if ( tempTrial.match( /(NCT|nct)[0-9]+/g ) ) { nctId = tempTrial; if ( this.trialListIds.includes( tempTrial ) ) { if (!this.isRedownloadTrial(tempTrial)) { continue; } } - this.importTrialsFromNct(nctId, ''); - } else if ( tempTrial.match( /^\d+-\d+$/g ) ) { + this.importTrialsFromNct(nctId); + } else if ( tempTrial.match( /^\d+-\d+$/g ) && this.oncokb) { this.connectionService.getTrialByProtocolNo( tempTrial ).subscribe( ( res ) => { - protocolNo = res['msk_id']; nctId = res['tds_data']['nct_id']; if ( this.trialListIds.includes( nctId ) ) { if (!this.isRedownloadTrial(tempTrial + '/' + nctId)) { return; } } - this.importTrialsFromNct(nctId, protocolNo); + const mskInfo = { + protocol_no: res['msk_id'], + principal_investigator: { + full_name: res['tds_data']['primary_investigator']['full_name'], + credentials: res['tds_data']['primary_investigator']['credentials'], + email: res['tds_data']['primary_investigator']['email'], + url: res['tds_data']['primary_investigator']['msk_url'] + } + }; + this.importTrialsFromNct(nctId, mskInfo); }, ( error ) => { this.messages.push( tempTrial + ' not found' ); }); @@ -147,7 +162,7 @@ export class TrialComponent implements OnInit, AfterViewInit { 'Are you sure you want to overwrite this trial by loading file ' + id + '?' ); } - importTrialsFromNct(nctId: string, protocolNo: string) { + importTrialsFromNct(nctId: string, extraInfo?: object) { let setChosenTrial = false; this.connectionService.importTrials( nctId ).subscribe( ( res ) => { const trialInfo = res; @@ -155,8 +170,9 @@ export class TrialComponent implements OnInit, AfterViewInit { _.forEach( trialInfo[ 'arms' ], function( arm ) { if ( arm.arm_description !== null ) { armsInfo.push( { - arm_description: arm.arm_name, + arm_description: MainUtil.normalizeText(arm.arm_name), arm_info: arm.arm_description, + drugs: [[]], match: [] } ); } @@ -164,8 +180,11 @@ export class TrialComponent implements OnInit, AfterViewInit { const trial: Trial = { curation_status: 'In progress', archived: 'No', - protocol_no: protocolNo, + protocol_no: '', nct_id: trialInfo[ 'nct_id' ], + principal_investigator: { + full_name: trialInfo[ 'principal_investigator' ] + }, long_title: trialInfo[ 'official_title' ], short_title: trialInfo[ 'brief_title' ], phase: trialInfo[ 'phase' ][ 'phase' ], @@ -177,21 +196,26 @@ export class TrialComponent implements OnInit, AfterViewInit { } ] } }; - this.db.object( 'Trials/' + trialInfo[ 'nct_id' ] ).set( trial ).then( ( response ) => { - this.messages.push( 'Successfully imported ' + trialInfo[ 'nct_id' ] ); + if (extraInfo) { + _.forEach(extraInfo, (value, key) => { + trial[key] = value; + }); + } + this.db.object( 'Trials/' + trial.nct_id ).set( trial ).then( ( response ) => { + this.messages.push( 'Successfully imported ' + trial.nct_id ); if (this.oncokb) { const metaRecord: Meta = { - protocol_no: protocolNo, - nct_id: trialInfo[ 'nct_id' ], - title: trialInfo[ 'official_title' ], - status: trialInfo[ 'current_trial_status' ], + protocol_no: trial.protocol_no, + nct_id: trial.nct_id, + title: trial.long_title, + status: trial.status, precision_medicine: 'YES', curated: 'YES' }; this.metaService.setMetaRecord(metaRecord); } if ( setChosenTrial === false ) { - this.nctIdChosen = trialInfo[ 'nct_id' ]; + this.nctIdChosen = trial.nct_id; this.trialService.setTrialChosen( this.nctIdChosen ); this.originalTrial = _.clone(this.trialChosen); setChosenTrial = true; @@ -230,11 +254,10 @@ export class TrialComponent implements OnInit, AfterViewInit { } } curateTrial( nctId: string ) { - this.protocolNoMessage.content = ''; + this.clearMessages(); this.clearAdditional(); this.trialService.setTrialChosen( nctId ); this.trialService.setAdditionalChosen( nctId ); - this.originalTrial = _.clone(this.trialChosen); document.querySelector( '#trialDetail' ).scrollIntoView(); } clearAdditional() { @@ -309,30 +332,53 @@ export class TrialComponent implements OnInit, AfterViewInit { this.noteEditable = false; } updateProtocolNo() { - if ( this.trialChosen['protocol_no'].match( /^\d+-\d+$/g ) ) { - const result = confirm('Are you sure to update Protocol No. to ' + this.trialChosen['protocol_no'] + '?'); - if (result) { - this.trialService.getRef( 'Trials/' + this.nctIdChosen ).update( {protocol_no: this.trialChosen['protocol_no']} ) - .then((res) => { - this.protocolNoMessage.content = 'Update Protocol No. successfully.'; - this.protocolNoMessage.color = 'green'; - }) - .catch( ( error ) => { - this.protocolNoMessage.content = 'Failed to update Protocol No.'; - this.protocolNoMessage.color = 'red'; - this.trialChosen['protocol_no'] = this.originalTrial['protocol_no']; - } ); + if (this.originalTrial['protocol_no'] !== this.trialChosen['protocol_no']) { + if ( this.trialChosen['protocol_no'].match( /^\d+-\d+$/g ) ) { + const result = confirm('Are you sure to update Protocol No. to ' + this.trialChosen['protocol_no'] + '?'); + if (result) { + this.trialService.getRef( 'Trials/' + this.nctIdChosen ).update( {protocol_no: this.trialChosen['protocol_no']} ) + .then((res) => { + this.protocolNoMessage = { + content: 'Update Protocol No. successfully.', + color: 'green' + }; + }) + .catch( ( error ) => { + this.protocolNoMessage = { + content: 'Failed to update Protocol No.', + color: 'red' + }; + this.trialChosen['protocol_no'] = this.originalTrial['protocol_no']; + } ); + } + } else { + this.protocolNoMessage = { + content: 'Protocol No. should follow the format: number-number.', + color: 'red' + }; + this.trialChosen['protocol_no'] = this.originalTrial['protocol_no']; } - } else { - this.protocolNoMessage.content = 'Protocol No. should follow the format: number-number.'; - this.protocolNoMessage.color = 'red'; - this.trialChosen['protocol_no'] = this.originalTrial['protocol_no']; } } - clearMessage(type: string) { - if (type === 'protocol_no') { - this.protocolNoMessage.content = ''; - this.protocolNoMessage.color = ''; + clearMessages() { + const emptyMessage: Message = { + content: '', + color: '' + }; + this.protocolAccessedMessage = emptyMessage; + this.protocolNoMessage = emptyMessage; + } + clearMessageByName(name: string) { + if (name === 'protocolAccessed') { + this.protocolAccessedMessage = { + content: '', + color: '' + }; + } else if (name === 'protocolNo') { + this.protocolNoMessage = { + content: '', + color: '' + }; } } download() { @@ -347,4 +393,37 @@ export class TrialComponent implements OnInit, AfterViewInit { }); saveAs(blob, 'TrialTable.xls'); } + updateProtocolAccessedDate() { + const nowTimestamp = MainUtil.updateTimestampByToday(); + this.trialService.getRef( 'Trials/' + this.nctIdChosen ).update( {protocol_accessed: nowTimestamp} ) + .then((res) => { + this.trialChosen.protocol_accessed = nowTimestamp; + this.clearMessageByName('protocolAccessed'); + }) + .catch( ( error ) => { + this.protocolAccessedMessage = { + content: 'Failed to update Last Update.', + color: 'red' + }; + }); + } + togglePencilIcon() { + this.displayPencil = !this.displayPencil; + } + saveModification(key: string) { + const objectToUpdate = {}; + objectToUpdate[key] = this.trialChosen[key]; + this.trialService.getRef( 'Trials/' + this.nctIdChosen ).update( objectToUpdate ) + .then((res) => { + this.displayPencil = true; + this.trialService.setTrialChosen(this.nctIdChosen); + }) + .catch( ( error ) => { + this.trialChosen.short_title = this.originalTrial.short_title; + } ); + } + cancelModification() { + this.trialChosen.short_title = this.originalTrial.short_title; + this.displayPencil = true; + } } diff --git a/src/main/webapp/app/trial/trial.model.ts b/src/main/webapp/app/trial/trial.model.ts index e192b25b..359a89a8 100644 --- a/src/main/webapp/app/trial/trial.model.ts +++ b/src/main/webapp/app/trial/trial.model.ts @@ -1,14 +1,22 @@ export interface Trial { curation_status: string; archived: string; + protocol_accessed?: number; protocol_no: string; nct_id: string; + principal_investigator?: PrincipalInvestigator; long_title: string; short_title: string; phase: string; status: string; treatment_list: { step: Array }; } +interface PrincipalInvestigator { + full_name: string; + credentials?: string; + email?: string; + url?: string; +} interface Step { match?: Array; arm?: Array; @@ -30,3 +38,8 @@ interface Match { } export interface Additional { note?: string; } + +export interface Message { + content: string; + color: string; +} diff --git a/src/main/webapp/app/trial/trial.scss b/src/main/webapp/app/trial/trial.scss index e089ef7e..8204c8fa 100644 --- a/src/main/webapp/app/trial/trial.scss +++ b/src/main/webapp/app/trial/trial.scss @@ -82,6 +82,13 @@ label { .download-button { margin: 15px 0 0 0; } +.trial-margin { + margin-top: 30px; +} #trialDetail { margin: 30px 0; } +.max-input { + max-width: 700px; + margin-top: 20px; +} diff --git a/src/main/webapp/content/scss/global.scss b/src/main/webapp/content/scss/global.scss index e6cfa939..3d21483d 100644 --- a/src/main/webapp/content/scss/global.scss +++ b/src/main/webapp/content/scss/global.scss @@ -223,3 +223,53 @@ ui bootstrap tweaks } /* jhipster-needle-scss-add-main JHipster will add new css style */ + +/* common css classes start */ +input[type="radio"], input[type="checkbox"] { + margin-right: 5px; +} +input[type="text"], textarea { + width: 100%; +} +.addIcon { + color: green; + margin-right:8px +} +.deleteIcon { + color: red; + margin-right:8px +} +.editIcon { + margin-right:8px +} +.exchangeIcon { + color: green; + margin-right:8px +} +.moveIcon { + font-size:14px; + color: orange; + margin-right:8px +} +.copyIcon { + color: #3E8ACC; + margin-right:8px +} +.destinationIcon { + color: green; + margin-right:8px +} +.drug-wrapper{ + display: inline-flex; + width: 100%; + + .drug-container{ + width: 50%; + margin-bottom: 10px; + } + .deleteIcon { + margin: 10px; + } +} + +/* common css classes end */ diff --git a/yarn.lock b/yarn.lock index 2d9d51cb..3c966066 100644 --- a/yarn.lock +++ b/yarn.lock @@ -329,7 +329,6 @@ "@sentry/browser@^4.6.5": version "4.6.5" resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-4.6.5.tgz#ecae280400117fef039db1b678e110a7d3e98a10" - integrity sha512-sIbEDTdZeRN+jzCEHGBOdidjSv+ZmJ9VPfek+bnP5FZNyUYfaZRrwWG0sJPPb8SlhSPUQXSI1t1saRhvd+Gs4A== dependencies: "@sentry/core" "4.6.5" "@sentry/types" "4.5.3" @@ -339,7 +338,6 @@ "@sentry/core@4.6.5": version "4.6.5" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.6.5.tgz#70b4bcfc555f7eff80581a17baadac5500c2f75d" - integrity sha512-dT0FATtKAgd4dashwK+S2vYzCXIga3VJFJgkZVTK2aoy45E56ztxcbmNdI8O+3e67tGM5Il6CrD2fZg5yLty9A== dependencies: "@sentry/hub" "4.6.5" "@sentry/minimal" "4.6.5" @@ -350,7 +348,6 @@ "@sentry/hub@4.6.5": version "4.6.5" resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.6.5.tgz#451def7bc8d90d9cc007f58f364b3ce305c4701a" - integrity sha512-v9vee8s8C1fK/DPtNOzv6r+AMbPDOWfnasouNcBUkbQUSN5wUNyCDvt51QbWaw5kMMY5TSqjdVqY6gXQZI0APQ== dependencies: "@sentry/types" "4.5.3" "@sentry/utils" "4.6.5" @@ -359,7 +356,6 @@ "@sentry/minimal@4.6.5": version "4.6.5" resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.6.5.tgz#64433d2c9fda69eedbb61855a7ff8905f7b19218" - integrity sha512-tf+J+uUNmSgzC7d9JSN8Ekw1HeBAX87Efa/jyFbzLvaw80oibvTiLSLqDjQ9PgvyIzBUuuPImkS2NpvPQGWFtg== dependencies: "@sentry/hub" "4.6.5" "@sentry/types" "4.5.3" @@ -368,12 +364,10 @@ "@sentry/types@4.5.3": version "4.5.3" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.5.3.tgz#3350dce2b7f9b936a8c327891c12e3aef7bd8852" - integrity sha512-7ll1PAFNjrBNX9rzy3P2qAQrpQwHaDO3uKj735qsnGw34OtAS8Xr8WYrjI14f9fMPa/XIeWvMPb4GMic28V/ag== "@sentry/utils@4.6.5": version "4.6.5" resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.6.5.tgz#4c960524914311eb76bbd6ca7f80f4d98c04db7f" - integrity sha512-rTISJtRRbWsd3UE+TkA3QG1C0VzPKPW8w74tieBwYhtTCGmOHNwz2nDC/MZGbGj4OgDmNKKl4CCyQr88EX08hA== dependencies: "@sentry/types" "4.5.3" tslib "^1.9.3" @@ -8890,7 +8884,6 @@ tslib@^1.7.1, tslib@^1.8.1, tslib@^1.9.0: tslib@^1.9.3: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== tslint-loader@3.5.3: version "3.5.3"