Skip to content

Commit 351100e

Browse files
committed
polishing the split column procedure
1 parent 5155b61 commit 351100e

File tree

7 files changed

+133
-46
lines changed

7 files changed

+133
-46
lines changed

.github/workflows/build-tauri.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ${{ matrix.platform }}
1616
strategy:
1717
matrix:
18-
platform: [ macos-latest, windows-latest] # ubuntu-latest - waiting for upstream ashpd (2025-01-13)
18+
platform: [ macos-latest, windows-latest, ubuntu-latest] # - waiting for upstream ashpd (2025-01-13)
1919

2020
steps:
2121
- name: Checkout repository

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "phenoboard",
3-
"version": "0.5.101",
3+
"version": "0.5.102",
44
"scripts": {
55
"ng": "ng",
66
"start": "nx serve phenoboard --port 1420",

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "phenoboard"
3-
version = "0.5.101"
3+
version = "0.5.102"
44
description = "Curate cohorts of GA4GH Phenopackets"
55
authors = ["Peter N Robinson"]
66
edition = "2021"

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "phenoboard",
4-
"version": "0.5.101",
4+
"version": "0.5.102",
55
"identifier": "org.p2gx.phenoboard",
66
"build": {
77
"beforeDevCommand": "npx nx serve phenoboard --configuration=development --no-cloud",
Lines changed: 107 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,125 @@
1-
import { Component, Inject } from '@angular/core';
2-
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
3-
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
41
import { CommonModule } from '@angular/common';
5-
import { MatInputModule } from '@angular/material/input';
6-
import { MatSelectModule } from '@angular/material/select';
2+
import { Component, computed, inject, signal } from '@angular/core';
3+
import { FormsModule } from '@angular/forms';
74
import { MatButtonModule } from '@angular/material/button';
5+
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
6+
import { MatRadioModule } from "@angular/material/radio";
87

98
@Component({
109
selector: 'app-split-column-dialog',
1110
standalone: true,
11+
imports: [CommonModule, MatDialogModule, MatRadioModule, MatButtonModule, FormsModule],
1212
template: `
13-
<h2 mat-dialog-title>Split Column</h2>
14-
<form [formGroup]="form" class="dialog-form">
15-
<mat-form-field class="w-full">
16-
<mat-label>Separator character</mat-label>
17-
<input matInput formControlName="separator" maxlength="3" placeholder="e.g. /,:,-, etc.">
18-
</mat-form-field>
19-
20-
<div class="dialog-actions">
21-
<button mat-button (click)="cancel()">Cancel</button>
22-
<button mat-flat-button color="primary" (click)="confirm()" [disabled]="form.invalid">OK</button>
23-
</div>
13+
<h2 mat-dialog-title class="!mb-0 text-slate-700">Split Column: {{data.originalHeader}}</h2>
14+
15+
<mat-dialog-content class="!pt-4">
16+
<div class="text-[10px] uppercase font-bold text-slate-400 mb-2 tracking-wider">Select Delimiter</div>
17+
<mat-radio-group
18+
[ngModel]="selectedDelimiter()"
19+
(ngModelChange)="selectedDelimiter.set($event)"
20+
class="grid grid-cols-2 gap-2 mb-3">
21+
<mat-radio-button value="," class="compact-radio">Comma (,)</mat-radio-button>
22+
<mat-radio-button value="/" class="compact-radio">Slash (/)</mat-radio-button>
23+
<mat-radio-button value="." class="compact-radio">Period (.)</mat-radio-button>
24+
<mat-radio-button value=" " class="compact-radio">Space</mat-radio-button>
25+
<mat-radio-button value="custom" class="compact-radio col-span-2">
26+
<span [class.text-indigo-600]="selectedDelimiter() === 'custom'" class="font-medium">Custom</span>
27+
</mat-radio-button>
28+
</mat-radio-group>
29+
30+
@if (selectedDelimiter() === 'custom') {
31+
<input
32+
class="w-full border-b border-indigo-500 outline-none py-1 mb-4 text-sm bg-transparent"
33+
[ngModel]="customValue()"
34+
(ngModelChange)="customValue.set($event)"
35+
placeholder="e.g. ; or |"
36+
autofocus>
37+
}
38+
39+
<div class="mt-4 p-3 bg-slate-900 rounded-lg shadow-inner overflow-hidden">
40+
<div class="text-[10px] text-indigo-300 uppercase font-black mb-2 tracking-widest">Row 1 Preview</div>
41+
<div class="flex-1">
42+
<div class="text-[9px] text-slate-500 mb-1 uppercase tracking-tighter font-bold">Original: </div>
43+
<div class="px-2 py-1.5 bg-slate-800 rounded text-xs font-mono text-emerald-400 truncate border border-slate-700">
44+
{{data.example }}
45+
</div>
46+
</div>
47+
<div class="flex items-center gap-2">
48+
<div class="flex-1">
49+
<div class="text-[9px] text-slate-500 mb-1 uppercase tracking-tighter font-bold">Part A</div>
50+
<div class="px-2 py-1.5 bg-slate-800 rounded text-xs font-mono text-emerald-400 truncate border border-slate-700">
51+
{{ splitExample().a }}
52+
</div>
53+
</div>
54+
55+
<div class="text-slate-600 font-bold self-end pb-1.5">→</div>
56+
57+
<div class="flex-1">
58+
<div class="text-[9px] text-slate-500 mb-1 uppercase tracking-tighter font-bold">Part B</div>
59+
<div class="px-2 py-1.5 bg-slate-800 rounded text-xs font-mono text-emerald-400 truncate border border-slate-700">
60+
{{ splitExample().b }}
61+
</div>
62+
</div>
63+
</div>
64+
</div>
65+
</mat-dialog-content>
66+
67+
<mat-dialog-actions align="end" class="!pb-4 !px-6">
68+
<button mat-button (click)="cancel()" class="text-slate-500 font-medium">Cancel</button>
69+
<button
70+
mat-flat-button
71+
(click)="confirm()"
72+
[disabled]="!finalDelimiter()"
73+
class="!rounded-md !bg-indigo-600 shadow-md !text-white disabled:!bg-slate-200">
74+
Apply Split
75+
</button>
76+
</mat-dialog-actions>
2477
`,
25-
imports: [CommonModule, ReactiveFormsModule, MatDialogModule, MatInputModule, MatSelectModule, MatButtonModule],
2678
styles: [`
27-
.dialog-form { display: flex; flex-direction: column; gap: 1rem; width: 300px; }
28-
.dialog-actions { display: flex; justify-content: flex-end; gap: 1rem; margin-top: 1rem; }
29-
`]
79+
:host { display: block; width: 350px; }
80+
.compact-radio {
81+
@apply border border-slate-200 rounded-md p-1 transition-all hover:bg-slate-50;
82+
}
83+
.mat-mdc-radio-button.mat-mdc-radio-checked {
84+
@apply border-indigo-500 bg-indigo-50/50;
85+
}
86+
::ng-deep .mdc-label { font-size: 11px !important; }
87+
`],
3088
})
3189
export class SplitColumnDialogComponent {
32-
form: FormGroup;
33-
34-
constructor(
35-
private fb: FormBuilder,
36-
private dialogRef: MatDialogRef<SplitColumnDialogComponent>,
37-
@Inject(MAT_DIALOG_DATA) public data: any
38-
) {
39-
this.form = this.fb.group({
40-
separator: [data.separator ?? '/', Validators.required],
41-
});
42-
}
90+
private dialogRef = inject(MatDialogRef<SplitColumnDialogComponent>);
91+
public data = inject(MAT_DIALOG_DATA) as { originalHeader: string, example: string };
92+
93+
selectedDelimiter = signal<string>(',');
94+
customValue = signal<string>('');
95+
96+
finalDelimiter = computed(() => {
97+
return this.selectedDelimiter() === 'custom' ? this.customValue() : this.selectedDelimiter();
98+
});
99+
100+
splitExample = computed(() => {
101+
const sep = this.finalDelimiter();
102+
const orig = this.data.example || '';
103+
104+
// If no separator is found in the string, show the whole thing in Part A and N/A in Part B
105+
if (!sep || !orig.includes(sep)) {
106+
return { a: orig || '---', b: 'n/a' };
107+
}
108+
109+
const index = orig.indexOf(sep);
110+
return {
111+
a: orig.substring(0, index),
112+
b: orig.substring(index + sep.length)
113+
};
114+
});
43115

44116
confirm() {
45-
this.dialogRef.close(this.form.value);
117+
this.dialogRef.close({
118+
separator: this.finalDelimiter()
119+
});
46120
}
47121

48122
cancel() {
49123
this.dialogRef.close(null);
50124
}
51-
}
125+
}

src/app/tableeditor/tableeditor.component.html

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ <h1>External Table Editor</h1>
143143
Merge
144144
</button>
145145
</div>
146+
<label class="flex items-center gap-1 cursor-pointer">
147+
<input
148+
type="checkbox"
149+
[checked]="labelMergedColumn()"
150+
(change)="labelMergedColumn.set($any($event.target).checked)"
151+
class="accent-indigo-500 w-3 h-3"
152+
/>
153+
<span class="text-[10px] text-white">Label merged values (A) ... (B) ...</span>
154+
</label>
146155
<button (click)="colAforMerge.set(null); colBforMerge.set(null)"
147156
class="text-[10px] text-indigo-200 hover:text-white text-right underline">
148157
Cancel
@@ -178,12 +187,12 @@ <h1>External Table Editor</h1>
178187
{{ col.header.original }}
179188

180189
@if (colAforMerge() === i) {
181-
<span class="absolute top-0 right-0 bg-yellow-600 text-white text-[10px] px-1 font-bold rounded-bl shadow-sm">
190+
<span class="absolute top-0 right-0 bg-yellow-600 text-white text-[18px] px-1 font-bold rounded-bl shadow-sm">
182191
A
183192
</span>
184193
}
185194
@if (colBforMerge() === i) {
186-
<span class="absolute top-0 right-0 bg-orange-600 text-white text-[10px] px-1 font-bold rounded-bl shadow-sm">
195+
<span class="absolute top-0 right-0 bg-orange-600 text-white text-[18px] px-1 font-bold rounded-bl shadow-sm">
187196
B
188197
</span>
189198
}

src/app/tableeditor/tableeditor.component.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export class TableEditorComponent implements OnInit {
124124
private lastSnapshot = signal<any[] | null>(null);
125125
undoVisible = signal(false);
126126
mergeSeparator = signal<string>(' ');
127+
// if this is true, add (A) original A value (B) original B value
128+
labelMergedColumn = signal<boolean>(false);
127129

128130
columnTypeCategories: TransformType[] = [
129131
TransformType.RAW_COLUMN_TYPE,
@@ -922,17 +924,17 @@ export class TableEditorComponent implements OnInit {
922924
const columns = dto.table.columns;
923925
const originalColumn = columns[index];
924926
if (!originalColumn) return;
927+
if (originalColumn.values.length < 1) return;
928+
const example = originalColumn.values[0].original;
925929

926930
const dialogRef = this.dialog.open(SplitColumnDialogComponent, {
927931
width: '400px',
928-
data: { separator: '/' }
932+
data: { originalHeader: originalColumn.header.original, example: example }
929933
});
930934

931935
dialogRef.afterClosed().subscribe((result) => {
932936
if (!result) return; // user cancelled
933-
934-
let separator = String(result.separator ?? '').trim();
935-
if (!separator) separator = " "; // default split
937+
const separator = result;
936938

937939
let columnAPosition = Number(result.sexPosition);
938940
let columnBPosition = Number(result.agePosition);
@@ -945,8 +947,8 @@ export class TableEditorComponent implements OnInit {
945947
columnA.id = crypto.randomUUID();
946948
columnB.id = crypto.randomUUID();
947949

948-
columnA.header = { ...columnA.header, original: `Part 1 (${originalColumn.header.original})` };
949-
columnB.header = { ...columnB.header, original: `Part 2 (${originalColumn.header.original})` };
950+
columnA.header = { ...columnA.header, original: `(A): ${originalColumn.header.original}` };
951+
columnB.header = { ...columnA.header, original: `(B): ${originalColumn.header.original}` };
950952

951953
// convert split strings into EtlCellValue objects
952954
columnA.values = originalColumn.values.map(cell => {
@@ -1618,6 +1620,7 @@ export class TableEditorComponent implements OnInit {
16181620
}
16191621
this.lastSnapshot.set([...dto.table.columns]);
16201622
const sep = this.mergeSeparator();
1623+
const labelAB = this.labelMergedColumn();
16211624
try {
16221625
const colA = dto.table.columns[idxA];
16231626
const colB = dto.table.columns[idxB];
@@ -1631,9 +1634,10 @@ export class TableEditorComponent implements OnInit {
16311634
values: colA.values.map((cell, i) => {
16321635
const valA = cell.original ?? '';
16331636
const valB = colB.values[i]?.original ?? '';
1637+
const mergedValue = labelAB ? `(A) ${valA} ${sep} (B) ${valB}`.trim() : `${valA}${sep}${valB}`.trim();
16341638
return {
16351639
...cell,
1636-
original: `(A) ${valA} ${sep} (B) ${valB}`.trim(),
1640+
original: mergedValue,
16371641
status: EtlCellStatus.Transformed
16381642
};
16391643
})

0 commit comments

Comments
 (0)