-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #340 from hubmapconsortium/cde-create-visualizatio…
…n-page Cde create visualization page
- Loading branch information
Showing
32 changed files
with
1,370 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
/* /index.html 200 | ||
/* /index.html 200 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1 @@ | ||
<cde-header></cde-header> | ||
<router-outlet></router-outlet> | ||
<cde-footer></cde-footer> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
apps/cde-ui/src/app/components/empty-form-control/empty-form-control.directive.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { FormControl, ReactiveFormsModule } from '@angular/forms'; | ||
import { render, screen } from '@testing-library/angular'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { MarkEmptyFormControlDirective } from './empty-form-control.directive'; | ||
|
||
describe('EmptyFormControlDirective', () => { | ||
it('initial state should have class "empty" when the control is empty', async () => { | ||
await render(`<input type="text" cdeMarkEmptyFormControl [formControl]="control" data-testid="input" />`, { | ||
imports: [MarkEmptyFormControlDirective, ReactiveFormsModule], | ||
componentProperties: { | ||
control: new FormControl(), | ||
}, | ||
}); | ||
|
||
const input = screen.getByTestId('input'); | ||
expect(input).toHaveClass('empty'); | ||
}); | ||
|
||
it('should remove "empty" class when the control is not empty', async () => { | ||
const user = userEvent.setup(); | ||
await render(`<input type="text" cdeMarkEmptyFormControl [formControl]="control" data-testid="input" />`, { | ||
imports: [MarkEmptyFormControlDirective, ReactiveFormsModule], | ||
componentProperties: { | ||
control: new FormControl(), | ||
}, | ||
}); | ||
|
||
const input = screen.getByTestId('input'); | ||
await user.type(input, 'Hello'); | ||
|
||
expect(input).not.toHaveClass('empty'); | ||
}); | ||
}); |
27 changes: 27 additions & 0 deletions
27
apps/cde-ui/src/app/components/empty-form-control/empty-form-control.directive.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Directive, OnInit, inject, signal } from '@angular/core'; | ||
import { NgControl } from '@angular/forms'; | ||
|
||
/** | ||
* Directive that adds an `empty` class to form control elements | ||
* when the control has no value. | ||
*/ | ||
@Directive({ | ||
selector: '[cdeMarkEmptyFormControl]', | ||
standalone: true, | ||
// eslint-disable-next-line @angular-eslint/no-host-metadata-property | ||
host: { | ||
'[class.empty]': 'isEmpty()', | ||
}, | ||
}) | ||
export class MarkEmptyFormControlDirective implements OnInit { | ||
/** Whether the form is empty */ | ||
readonly isEmpty = signal(true); | ||
|
||
/** Reference to the form control */ | ||
private readonly control = inject(NgControl, { self: true }); | ||
|
||
/** Binds the form control state to the isEmpty signal */ | ||
ngOnInit(): void { | ||
this.control.valueChanges?.subscribe((value) => this.isEmpty.set(!value)); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
apps/cde-ui/src/app/components/file-upload/file-upload.component.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<div class="column-content"> | ||
@if (!file && fileTitle(); as title) { | ||
<h3 class="subtitle">{{ fileTitle() }}</h3> | ||
} | ||
@if (file) { | ||
<div class="upload-success"> | ||
<div class="filename"> | ||
@if (fileTitle(); as title) { | ||
<h3 class="subtitle">File Loaded:</h3> | ||
} @else { | ||
<span class="loaded-label">File loaded:</span> | ||
} | ||
<span class="column-info-list mat-body-2 file-name">{{ file.name }}</span> | ||
</div> | ||
<button class="remove-file" mat-flat-button (click)="cancelLoad()" type="button" color="primary"> | ||
<mat-icon class="material-symbols-rounded">cancel</mat-icon> | ||
Remove CSV | ||
</button> | ||
</div> | ||
} @else { | ||
<button class="upload" mat-flat-button (click)="fileInput.click()" type="button" color="primary"> | ||
<mat-icon class="material-symbols-rounded">upload</mat-icon> | ||
Upload CSV | ||
</button> | ||
<input | ||
#fileInput | ||
(change)="load(fileInput)" | ||
type="file" | ||
[attr.accept]="accept()" | ||
name="fileInput" | ||
style="display: none" | ||
/> | ||
} | ||
</div> |
26 changes: 26 additions & 0 deletions
26
apps/cde-ui/src/app/components/file-upload/file-upload.component.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
:host { | ||
display: block; | ||
|
||
.upload-success { | ||
display: flex; | ||
justify-content: space-between; | ||
|
||
.filename span { | ||
display: block; | ||
} | ||
|
||
.file-name { | ||
font-weight: 300; | ||
} | ||
|
||
.remove-file { | ||
align-self: flex-end; | ||
} | ||
} | ||
|
||
.upload-colormap .loaded-label { | ||
font-weight: 500; | ||
font-size: 1.5rem; | ||
line-height: 2.25rem; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
apps/cde-ui/src/app/components/file-upload/file-upload.component.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { FileLoaderOptions, FileLoaderResult, FileUploadComponent } from './file-upload.component'; | ||
import { render, screen } from '@testing-library/angular'; | ||
import '@testing-library/jest-dom'; | ||
import { signal } from '@angular/core'; | ||
|
||
describe('FileUploadComponent', () => { | ||
let loader: jest.Mock<FileLoaderResult<unknown>, [File, FileLoaderOptions]>; | ||
|
||
beforeEach(() => { | ||
loader = jest.fn(); | ||
loader.mockReturnValue({ | ||
progress: signal(0), | ||
result: Promise.resolve('abc'), | ||
}); | ||
}); | ||
|
||
test('should load', async () => { | ||
await render(FileUploadComponent, { | ||
componentInputs: { | ||
fileTitle: 'testTitle', | ||
accept: 'csv', | ||
loader: loader, | ||
}, | ||
}); | ||
|
||
expect(screen.getByText(/testTitle/i)).toBeInTheDocument(); | ||
}); | ||
}); |
138 changes: 138 additions & 0 deletions
138
apps/cde-ui/src/app/components/file-upload/file-upload.component.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { CommonModule } from '@angular/common'; | ||
import { ChangeDetectionStrategy, Component, Injector, Signal, effect, inject, input, output } from '@angular/core'; | ||
import { MatButtonModule } from '@angular/material/button'; | ||
import { MatIconModule } from '@angular/material/icon'; | ||
|
||
/** Loader options */ | ||
export interface FileLoaderOptions { | ||
/** Signal to stop loading */ | ||
signal: AbortSignal; | ||
} | ||
|
||
/** Result from a file loader callback */ | ||
export interface FileLoaderResult<T> { | ||
/** Signal of current progress in range [0, 1] */ | ||
progress: Signal<number>; | ||
/** Promise resolving to the loaded data */ | ||
result: Promise<T>; | ||
} | ||
|
||
/** File loader function */ | ||
export type FileLoader<T> = (file: File, options: FileLoaderOptions) => FileLoaderResult<T>; | ||
|
||
/** Cleanup function */ | ||
type CleanupFn = () => void; | ||
|
||
/** Component for loading a file from disk */ | ||
@Component({ | ||
selector: 'cde-file-upload', | ||
standalone: true, | ||
imports: [CommonModule, MatIconModule, MatButtonModule], | ||
templateUrl: './file-upload.component.html', | ||
styleUrl: './file-upload.component.scss', | ||
changeDetection: ChangeDetectionStrategy.OnPush, | ||
}) | ||
export class FileUploadComponent<T> { | ||
/** Title */ | ||
readonly fileTitle = input<string>(); | ||
/** Accepted file types */ | ||
readonly accept = input.required<string>(); | ||
/** File loader */ | ||
readonly loader = input.required<FileLoader<T>>(); | ||
|
||
/** Progress events */ | ||
readonly progress = output<number>(); | ||
/** Loading start events */ | ||
readonly loadStarted = output<File>(); | ||
/** Loading cancelled events */ | ||
readonly loadCancelled = output<void>(); | ||
/** Loading error events */ | ||
readonly loadErrored = output<unknown>(); | ||
/** Loading completed events */ | ||
readonly loadCompleted = output<T>(); | ||
|
||
/** Reference to injector */ | ||
private readonly injector = inject(Injector); | ||
|
||
/** Loaded file */ | ||
file?: File; | ||
/** Cleanup functions */ | ||
private cleanup: CleanupFn[] = []; | ||
|
||
/** | ||
* Loads a file | ||
* | ||
* @param el Input element | ||
*/ | ||
load(el: HTMLInputElement): void { | ||
if (el.files === null) { | ||
return; | ||
} | ||
|
||
const { loader, injector, progress, loadStarted } = this; | ||
const file = (this.file = el.files[0]); | ||
const cleanup: CleanupFn[] = (this.cleanup = []); | ||
const abortController = new AbortController(); | ||
const options: FileLoaderOptions = { | ||
signal: abortController.signal, | ||
}; | ||
const result = loader()(file, options); | ||
let done = false; | ||
|
||
el.files = null; | ||
loadStarted.emit(file); | ||
const progressRef = effect(() => progress.emit(result.progress()), { injector, manualCleanup: true }); | ||
this.handleResult(result.result, cleanup, () => done); | ||
|
||
cleanup.push( | ||
() => (done = true), | ||
() => progressRef.destroy(), | ||
() => abortController.abort(), | ||
); | ||
} | ||
|
||
/** | ||
* Cancels the currently loading file | ||
*/ | ||
cancelLoad(): void { | ||
this.runCleanup(this.cleanup); | ||
this.file = undefined; | ||
this.loadCancelled.emit(); | ||
} | ||
|
||
/** | ||
* Runs a set of cleanup functions | ||
* | ||
* @param fns Cleanup functions to run | ||
*/ | ||
private runCleanup(fns: CleanupFn[]): void { | ||
for (const fn of fns) { | ||
fn(); | ||
} | ||
} | ||
|
||
/** | ||
* Processes the result of a loader | ||
* | ||
* @param result Promise resolving to the loaded data | ||
* @param cleanup Cleanup function to run at the end | ||
* @param isDone Query function returning whether the loading has cancelled | ||
*/ | ||
private async handleResult(result: Promise<T>, cleanup: CleanupFn[], isDone: () => boolean): Promise<void> { | ||
const { progress, loadErrored, loadCompleted } = this; | ||
try { | ||
const data = await result; | ||
if (!isDone()) { | ||
progress.emit(1); | ||
loadCompleted.emit(data); | ||
} | ||
} catch (reason) { | ||
if (!isDone()) { | ||
this.file = undefined; | ||
loadErrored.emit(reason); | ||
} | ||
} finally { | ||
this.runCleanup(cleanup); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.