Skip to content

Commit 13e2ca9

Browse files
committed
Allow arbitrary number of melded diseases
1 parent 54eb3fc commit 13e2ca9

File tree

7 files changed

+153
-103
lines changed

7 files changed

+153
-103
lines changed

src/app/cohortdialog/cohortdialog.component.html

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,24 @@ <h3>Gene Info</h3>
119119
'The fields must be 1. <b>Disease name (Phenotype)</b>, 2. <b>OMIM id</b>, 3. skipped, 4. skipped, 5. <b>Gene symbol</b>.',
120120
'For instance, the data from the Phenotype-Gene Relationships table in OMIM can be copied and pasted here.'
121121
]" />
122-
<button mat-button (click)="cancel()" class="btn-outline-cancel">Cancel</button>
123-
<button mat-raised-button [disabled]="form.invalid" (click)="submit()" class="btn-primary" style="margin-left: 16px;">OK</button>
122+
<div class="flex justify-end gap-2 mt-6">
123+
<button type="button" class="btn-outline-cancel" (click)="cancel()">Cancel</button>
124+
@if (data.isMelded) {
125+
<button type="button"
126+
class="btn-secondary"
127+
(click)="submitAndAddNext()"
128+
[disabled]="form.invalid">
129+
<span class="text-lg leading-none">+</span>
130+
Add Another Disease
131+
</button>
132+
}
133+
134+
<button type="submit"
135+
class="btn-primary"
136+
(click)="submit()"
137+
[disabled]="form.invalid">
138+
Finish
139+
</button>
140+
</div>
124141
</mat-dialog-actions>
125142
}

src/app/cohortdialog/cohortdialog.component.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { HelpButtonComponent } from "../util/helpbutton/help-button.component";
99
import { ConfigService } from '../services/config.service';
1010
import { toSignal } from '@angular/core/rxjs-interop';
1111

12+
export interface CohortDialogData {
13+
title: string;
14+
isMelded: boolean; // Add this
15+
}
1216

1317
@Component({
1418
selector: 'app-cohort-dialog',
@@ -30,7 +34,7 @@ export class CohortDialogComponent {
3034
pastedText = signal<string | null>(null);
3135
private fb = inject(FormBuilder);
3236
public dialogRef = inject(MatDialogRef<CohortDialogComponent>);
33-
public data = inject(MAT_DIALOG_DATA) as { title: string };
37+
public data = inject(MAT_DIALOG_DATA) as CohortDialogData;
3438
private configService = inject(ConfigService);
3539

3640
form: FormGroup = this.fb.group({
@@ -52,9 +56,28 @@ export class CohortDialogComponent {
5256
this.dialogRef.close(null);
5357
}
5458

59+
60+
61+
// For melded, submit but expect to add another disease
62+
submitAndAddNext() {
63+
if (this.form.valid) {
64+
// We pass back the form value AND a flag to continue
65+
this.dialogRef.close({
66+
entry: this.form.value,
67+
keepGoing: true
68+
});
69+
} else {
70+
this.form.markAllAsTouched();
71+
}
72+
}
73+
74+
5575
submit() {
5676
if (this.form.valid) {
57-
this.dialogRef.close(this.form.value);
77+
this.dialogRef.close({
78+
entry: this.form.value,
79+
keepGoing: false
80+
});
5881
} else {
5982
this.form.markAllAsTouched();
6083
}

src/app/newtemplate/display-melded.component.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@ import { DiseaseData } from "../models/cohort_dto";
1010
<div class="summary-card">
1111
<p class="font-bold text-purple-700 mb-2">Melded Cohort: {{ acronym() }}</p>
1212
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
13-
<div>
14-
<span class="text-xs font-bold uppercase text-gray-500">Source A</span>
15-
<app-display-disease [disease]="diseaseA()" />
16-
</div>
17-
<div>
18-
<span class="text-xs font-bold uppercase text-gray-500">Source B</span>
19-
<app-display-disease [disease]="diseaseB()" />
20-
</div>
13+
@for (disease of diseases(); track $index) {
14+
<div>
15+
<span class="text-xs font-bold uppercase text-gray-500">Disease {{ $index + 1 }}</span>
16+
<app-display-disease [disease]="disease" />
17+
</div>
18+
}
2119
</div>
2220
</div>
2321
`
2422
})
2523
export class DisplayMeldedComponent {
26-
diseaseA = input.required<DiseaseData>();
27-
diseaseB = input.required<DiseaseData>();
24+
diseases = input.required<DiseaseData[]>();
2825
acronym = input.required<string>();
2926
}

src/app/newtemplate/newtemplate.component.html

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,17 @@ <h2 class="text-xl font-bold text-gray-800">🧬 {{ type | titlecase }} Cohort</
7171

7272
@switch (type) {
7373
@case ('mendelian') {
74-
@if (diseaseDataA(); as data) {
74+
@if (mendelianDisease(); as data) {
7575
<app-display-mendelian [disease]="data" [acronym]="cohort?.cohortAcronym ?? ''" />
7676
}
7777
}
7878
@case ('melded') {
79-
@if (diseaseDataA(); as a) {
80-
@if (diseaseDataB(); as b) {
81-
<app-display-melded [diseaseA]="a" [diseaseB]="b" [acronym]="cohort?.cohortAcronym ?? ''" />
82-
}
83-
}
79+
<div class="space-y-4">
80+
<h3 class="text-md font-semibold text-gray-700">Melded diseases:</h3>
81+
<app-display-melded
82+
[diseases]="diseasesData()"
83+
[acronym]="cohortAcronym()" />
84+
</div>
8485
}
8586
}
8687

src/app/newtemplate/newtemplate.component.ts

Lines changed: 64 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -69,67 +69,61 @@ export class NewTemplateComponent {
6969
meldedTemplate = false;
7070
mendelianTemplate = false;
7171

72-
diseaseA = signal<CohortEntry | null>(null);
73-
diseaseB = signal<CohortEntry | null>(null);
74-
diseaseDataA = computed<DiseaseData | null>(() => {
75-
const d = this.diseaseA();
76-
if (!d) return null;
77-
else return toDiseaseData(d);
72+
diseases = signal<CohortEntry[]>([]);
73+
diseasesData = computed<DiseaseData[]>(() =>
74+
this.diseases().map(d => toDiseaseData(d))
75+
);
76+
mendelianDisease = computed<DiseaseData | null>(() => {
77+
const list = this.diseasesData();
78+
return list.length > 0 ? list[0] : null;
7879
});
79-
diseaseDataB = computed<DiseaseData | null>(() => {
80-
const d = this.diseaseB();
81-
if (!d) return null;
82-
else return toDiseaseData(d);
80+
cohortAcronym = computed(() => {
81+
const currentDiseases = this.diseases();
82+
if (currentDiseases.length === 0) return '';
83+
84+
return currentDiseases
85+
.map(d => `${d.symbol}_${d.cohortAcronym}`)
86+
.join("_");
8387
});
88+
8489
thisCohortType = signal<CohortType | null>(null);
8590
pendingCohort = signal<CohortData | null>(null);
8691
showSuccessMessage = signal<boolean>(false);
8792

8893

89-
90-
91-
9294
async melded(): Promise<void> {
93-
this.mendelianTemplate = false;
94-
this.meldedTemplate = true;
95-
this.digenicTemplate = false;
96-
this.resetCohort();
97-
const first = this.dialog.open(CohortDialogComponent, {
98-
width: '450px',
99-
height: '550px',
100-
data: { title: 'Enter Disease A Info' }
101-
});
102-
if (!first) return;
103-
const rawValueA = await firstValueFrom(first.afterClosed());
104-
if (rawValueA) {
105-
const entryA: CohortEntry = {
106-
...rawValueA,
107-
geneTranscriptList: [] // Ensure the optional list is initialized if needed
108-
};
109-
this.diseaseA.set(entryA);
110-
} else {
111-
this.notificationService.showError("Could not retrieve disease A");
112-
return;
95+
this.resetCohort();
96+
this.mendelianTemplate = false;
97+
this.meldedTemplate = true;
98+
this.digenicTemplate = false;
99+
const tempDiseases: CohortEntry[] = [];
100+
let shouldContinue = true;
101+
while (shouldContinue) {
102+
const dialogRef = this.dialog.open(CohortDialogComponent, {
103+
width: '450px',
104+
height: '700px',
105+
data: {
106+
title: `Enter disease #${tempDiseases.length + 1} Info`,
107+
isMelded: true
108+
}
109+
});
110+
const result = await firstValueFrom(dialogRef.afterClosed());
111+
if (! result) {
112+
shouldContinue = false;
113+
if (tempDiseases.length < 2) this.resetCohort();
113114
}
114-
115-
const second = await this.dialog.open(CohortDialogComponent, {
116-
width: '450px',
117-
height: '550px',
118-
data: { title: 'Enter Disease B Info' }
119-
});
120-
if (!second) return;
121-
const rawValueB = await firstValueFrom(second.afterClosed());
122-
if (rawValueB) {
123-
const entryB: CohortEntry = {
124-
...rawValueB,
125-
geneTranscriptList: []
126-
};
127-
this.diseaseB.set(entryB);
128-
} else {
129-
this.notificationService.showError("Could not retrieve disease B");
130-
return;
115+
if (result) {
116+
tempDiseases.push({ ...result.entry, geneTranscriptList: []});
117+
shouldContinue = result.keepGoing;
131118
}
132-
this.createMeldedTemplate();
119+
}
120+
if (tempDiseases.length >= 2) {
121+
this.diseases.set(tempDiseases);
122+
this.createMeldedTemplate();
123+
} else {
124+
this.notificationService.showError("Could not get >=2 diseases for melded cohort");
125+
this.resetCohort();
126+
}
133127
}
134128

135129
async digenic(): Promise<void> {
@@ -138,7 +132,7 @@ export class NewTemplateComponent {
138132
this.digenicTemplate = true;
139133
const dialogRef = this.dialog.open(CohortDialogComponent, {
140134
width: '450px',
141-
height: '550px',
135+
height: '700px',
142136
data: { title: 'Create Digenic Cohort', mode: 'digenic' }
143137
});
144138
const result = await firstValueFrom(dialogRef.afterClosed());
@@ -155,7 +149,7 @@ async mendelian(): Promise<void> {
155149

156150
const dialogRef = this.dialog.open(CohortDialogComponent, {
157151
width: '450px',
158-
height: '650px',
152+
height: '700px',
159153
data: { title: 'Create Mendelian Cohort', mode: 'mendelian' }
160154
});
161155
const rawValue = await firstValueFrom(dialogRef.afterClosed());
@@ -165,10 +159,10 @@ async mendelian(): Promise<void> {
165159
}
166160
if (rawValue) {
167161
const entryA: CohortEntry = {
168-
...rawValue,
162+
...rawValue.entry,
169163
geneTranscriptList: []
170164
};
171-
this.diseaseA.set(entryA);
165+
this.diseases.set([entryA]);
172166
} else {
173167
this.notificationService.showError("Could not retrieve disease");
174168
return;
@@ -178,43 +172,32 @@ async mendelian(): Promise<void> {
178172
}
179173

180174
private async createMeldedTemplate(): Promise<void> {
181-
const diseaseA = this.diseaseA();
182-
const diseaseB = this.diseaseB();
183-
if (!diseaseA) {
184-
this.notificationService.showError("Could not retrieve disease A data for melded template");
185-
return;
186-
}
187-
if (!diseaseB) {
188-
this.notificationService.showError("Could not retrieve disease B data for melded template");
175+
const currentDiseases = this.diseases();
176+
if (currentDiseases.length < 2) {
177+
this.notificationService.showError(`Only ${currentDiseases.length} diseases but need at least 2 for melded cohort`);
189178
return;
190179
}
191-
192-
const acronymA = diseaseA.cohortAcronym;
193-
const acronymB = diseaseB.cohortAcronym;
194-
const geneA = diseaseA.symbol;
195-
const geneB = diseaseB.symbol;
196-
const acronym = `${geneA}_${acronymA}_${geneB}_${acronymB}`;
197-
const cohort = await this.configService.createNewMeldedTemplate(toDiseaseData(diseaseA), toDiseaseData(diseaseB), acronym);
198-
this.resetCohort();
199-
this.pendingCohort.set(cohort);
200-
this.thisCohortType.set("melded");
180+
181+
182+
const cohort = await this.configService.createNewMeldedTemplate(this.diseasesData(), this.cohortAcronym());
183+
this.resetCohort();
184+
this.pendingCohort.set(cohort);
185+
this.thisCohortType.set("melded");
201186
}
202187

203188
private async createMendelianTemplate(): Promise<void> {
204189
this.etl_service.clearEtlDto();
205-
const diseaseA = this.diseaseA();
206-
if (! diseaseA) {
207-
this.notificationService.showError("Could not retrieve disease for Mendelian");
190+
const diseases = this.diseases();
191+
if (diseases.length != 1) {
192+
this.notificationService.showError(`Expected to get one disease for Mendelian cohort, but got ${diseases.length}`);
208193
return;
209194
}
210-
const acronym = diseaseA.cohortAcronym;
211-
const gene = diseaseA.symbol;
212-
const fullAcronym = `${gene}_${acronym}`;
195+
const disease = diseases[0];
213196
const ctype: CohortType = "mendelian";
214197
const template = await this.configService.createNewTemplate(
215-
toDiseaseData(diseaseA),
198+
toDiseaseData(disease),
216199
ctype,
217-
fullAcronym
200+
this.cohortAcronym()
218201
);
219202
this.resetCohort();
220203
this.pendingCohort.set(template);

src/app/services/config.service.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,7 @@ export class ConfigService {
5656
});
5757
}
5858

59-
async createNewMeldedTemplate(diseaseA: DiseaseData, diseaseB: DiseaseData, acronym: string): Promise<CohortData> {
60-
const diseaseList = [diseaseA, diseaseB];
59+
async createNewMeldedTemplate(diseaseList: DiseaseData[], acronym: string): Promise<CohortData> {
6160
return await invoke<CohortData>("create_new_melded_cohort",
6261
{ 'diseases': diseaseList, 'acronym': acronym}
6362
)

src/styles/_buttons.scss

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,34 @@ button.btn-primary {
180180
&:disabled {
181181
@apply bg-gray-200 text-gray-400 border-gray-200;
182182
}
183+
}
184+
185+
button.btn-secondary {
186+
@extend %btn-base;
187+
188+
// Using a soft Purple/Indigo theme
189+
background-color: #f5f3ff; // Violet-50
190+
color: #5b21b6; // Violet-800
191+
border-color: #ddd6fe; // Violet-200
192+
193+
&:hover:not(:disabled) {
194+
background-color: #ede9fe; // Violet-100
195+
color: #4c1d95; // Violet-900
196+
border-color: #c4b5fd; // Violet-300
197+
transform: translateY(-1px);
198+
}
199+
200+
&:focus {
201+
@apply outline-none ring-2;
202+
--tw-ring-color: #{rgba(#8b5cf6, 0.3)}; // Violet-500
203+
}
204+
205+
&:active:not(:disabled) {
206+
transform: translateY(0);
207+
background-color: #ddd6fe;
208+
}
209+
210+
&:disabled {
211+
@apply bg-gray-50 text-gray-300 border-gray-100;
212+
}
183213
}

0 commit comments

Comments
 (0)