adding a medical sources card - using it for medical-source-connected and medical-sources pages/component.

make sure additional fields/metadata (like aliases and category) are sent.
This commit is contained in:
Jason Kulatunga 2023-05-09 22:52:42 -07:00
parent 2db04a15da
commit 2d570850f0
12 changed files with 228 additions and 87 deletions

View File

@ -0,0 +1,16 @@
<div class="card h-100 d-flex align-items-center justify-content-center mt-3 mt-3 rounded-0 cursor-pointer">
<div (click)="onCardClick()" class="card-body">
<div class="h-100 d-flex align-items-center">
<img [src]="'assets/sources/'+(sourceInfo?.metadata.brand_logo ? sourceInfo?.metadata?.brand_logo : sourceInfo?.metadata?.source_type+'.png')" [alt]="sourceInfo?.metadata?.display" class="img-fluid">
</div>
<div *ngIf="status" class="progress">
<div [style.width]="status == 'authorize' ? '33%' : '66%'" class="bg-indigo progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
</div>
</div>
<div class="card-footer text-center p-1" style="width:100%">
<small class="tx-gray-700">
{{sourceInfo?.metadata.display}}
</small>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MedicalSourcesCardComponent } from './medical-sources-card.component';
describe('MedicalSourcesCardComponent', () => {
let component: MedicalSourcesCardComponent;
let fixture: ComponentFixture<MedicalSourcesCardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MedicalSourcesCardComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(MedicalSourcesCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,25 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {SourceListItem} from '../../pages/medical-sources/medical-sources.component';
@Component({
selector: 'app-medical-sources-card',
templateUrl: './medical-sources-card.component.html',
styleUrls: ['./medical-sources-card.component.scss']
})
export class MedicalSourcesCardComponent implements OnInit {
@Input() sourceInfo: SourceListItem;
@Input() status: undefined | "token" | "authorize";
@Output() onClick = new EventEmitter<SourceListItem>()
constructor() { }
ngOnInit(): void {
}
onCardClick(){
this.onClick.emit(this.sourceInfo)
}
}

View File

@ -1,24 +1,13 @@
<h2 class="az-content-title">Connected Sources</h2>
<div *ngIf="!loading else isLoadingTemplate" class="row">
<div *ngFor="let sourceInfo of connectedSourceList" class="col-sm-3 mg-b-20 px-3">
<div class="card h-100 d-flex align-items-center justify-content-center mt-3 mt-3 rounded-0 cursor-pointer">
<div (click)="openModal(contentModalRef, sourceInfo)" class="card-body">
<div class="h-100 d-flex align-items-center">
<img [src]="'assets/sources/'+(sourceInfo.metadata.brand_logo ? sourceInfo.metadata.brand_logo : sourceInfo.metadata.source_type+'.png')" [alt]="sourceInfo?.metadata.display" class="img-fluid">
</div>
<div *ngIf="status[sourceInfo.metadata?.source_type]" class="progress">
<div [style.width]="status[sourceInfo?.metadata?.source_type] == 'authorize' ? '33%' : '66%'" class="bg-indigo progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
</div>
</div>
<div class="card-footer text-center p-1" style="width:100%">
<small class="tx-gray-700">
{{sourceInfo?.metadata.display}}
</small>
</div>
</div>
</div>
<app-medical-sources-card class="col-sm-3 mg-b-20 px-3"
*ngFor="let sourceData of connectedSourceList"
[sourceInfo]="sourceData"
[status]="status[sourceData.metadata.source_type]"
(onClick)="openModal(contentModalRef, $event)"
></app-medical-sources-card>
</div>
@ -43,7 +32,7 @@
<div class="modal-footer">
<button (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>
<!-- <button (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>-->
<button type="button" class="btn btn-outline-danger">Delete</button>
<button type="button" class="btn disabled btn-outline-danger">Delete</button>
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
</div>
</ng-template>

View File

@ -13,7 +13,7 @@ import {LighthouseService} from '../../services/lighthouse.service';
})
export class MedicalSourcesConnectedComponent implements OnInit {
loading: boolean = false
status: { [name: string]: string } = {}
status: { [name: string]: undefined | "token" | "authorize" } = {}
modalSelectedSourceListItem:SourceListItem = null;
modalCloseResult = '';

View File

@ -74,6 +74,7 @@ import {GridstackItemComponent} from './gridstack/gridstack-item.component';
import { MedicalSourcesFilterComponent } from './medical-sources-filter/medical-sources-filter.component';
import { MedicalSourcesConnectedComponent } from './medical-sources-connected/medical-sources-connected.component';
import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medical-sources-category-lookup.pipe';
import { MedicalSourcesCardComponent } from './medical-sources-card/medical-sources-card.component';
@NgModule({
imports: [
@ -160,6 +161,7 @@ import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medic
MedicalSourcesFilterComponent,
MedicalSourcesConnectedComponent,
MedicalSourcesCategoryLookupPipe,
MedicalSourcesCardComponent,
],
exports: [
BinaryComponent,
@ -208,6 +210,8 @@ import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medic
ResourceListOutletDirective,
ToastComponent,
UtilitiesSidebarComponent,
MedicalSourcesCardComponent,
MedicalSourcesConnectedComponent,
//standalone components
BadgeComponent,
@ -224,7 +228,7 @@ import { MedicalSourcesCategoryLookupPipe } from './medical-sources-filter/medic
BinaryComponent,
GridstackComponent,
GridstackItemComponent,
MedicalSourcesConnectedComponent,
MedicalSourcesCategoryLookupPipe,
]
})

View File

@ -1,8 +1,12 @@
export class MetadataSource {
platform_type: string
aliases?: string[]
brand_logo?: string
source_type: string
display: string
category: string[]
display: string
hidden: boolean
identifiers?: {[name:string]: string}
patient_access_description?: string
patient_access_url?: string
platform_type: string
source_type: string
}

View File

@ -71,21 +71,13 @@
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
>
<div *ngFor="let sourceData of availableSourceList" (click)="connectHandler($event, sourceData.metadata.source_type)" class="col-sm-3 mg-b-20 px-3">
<div class="card h-100 d-flex align-items-center justify-content-center mt-3 mt-3 rounded-0 cursor-pointer" [ngClass]="{'card-disable': sourceData.metadata.hidden}">
<div class="card-body d-flex align-items-center">
<img style="max-height: 130px;" [src]="'assets/sources/'+(sourceData.metadata.brand_logo ? sourceData.metadata.brand_logo : sourceData.metadata.source_type+'.png')" [alt]="sourceData.metadata.display" class="img-fluid">
<div *ngIf="status[sourceData.metadata.source_type]" class="progress">
<div [style.width]="status[sourceData.metadata.source_type] == 'authorize' ? '33%' : '66%'" class="bg-indigo progress-bar progress-bar-striped progress-bar-animated" role="progressbar"></div>
</div>
</div>
<div class="card-footer text-center p-1" style="width:100%">
<small class="tx-gray-700">
{{sourceData.metadata.display}}
</small>
</div>
</div>
</div>
<app-medical-sources-card class="col-sm-3 mg-b-20 px-3"
*ngFor="let sourceData of availableSourceList"
[sourceInfo]="sourceData"
[status]="status[sourceData.metadata.source_type]"
(onClick)="connectModalHandler(contentModalRef, $event)"
></app-medical-sources-card>
</div><!-- row -->
@ -95,6 +87,61 @@
</div><!-- container -->
</div><!-- az-content -->
<ng-template #contentModalRef let-modal>
<div class="modal-header">
<div class="media">
<img class="modal-header-media-image mg-sm-r-20 mg-b-20 mg-sm-b-0" [src]="'assets/sources/'+(modalSelectedSourceListItem?.metadata.brand_logo ? modalSelectedSourceListItem?.metadata?.brand_logo : modalSelectedSourceListItem?.metadata?.source_type+'.png')" [alt]="modalSelectedSourceListItem?.metadata?.display">
<div class="media-body">
<h6>{{modalSelectedSourceListItem?.metadata.display}}</h6>
<a *ngIf="modalSelectedSourceListItem?.metadata.patient_access_url"
[href]="modalSelectedSourceListItem.metadata?.patient_access_url"
target="_blank"
class="mg-b-0">{{modalSelectedSourceListItem?.metadata.patient_access_url}}</a>
</div><!-- media-body -->
</div><!-- media -->
<button type="button" class="btn btn-close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<h6>Connect Source</h6>
<p>Would you like to connect this healthcare institution with Fasten Health? You will be redirected to their patient portal,
where you can authenticate and select data to import into Fasten Health.
</p>
<p>
If the data about this institution is missing or incorrect, you can <a class="link" href="https://docs.google.com/spreadsheets/d/1ZSgwfd7kwxSnimk4yofIFcR8ZMUls0zi9SZpRiOJBx0/edit?usp=sharing">click here</a> to submit a correction.
</p>
<ng-container *ngIf="modalSelectedSourceListItem?.metadata?.category?.length > 0 || modalSelectedSourceListItem?.metadata?.patient_access_description">
<hr/>
<ng-container *ngIf="modalSelectedSourceListItem?.metadata?.patient_access_description">
<h6>About this Source</h6>
<p >{{modalSelectedSourceListItem?.metadata?.patient_access_description}}</p>
</ng-container>
<ng-container *ngIf="modalSelectedSourceListItem?.metadata?.category?.length > 0">
<h6>Categories</h6>
<ul>
<li *ngFor="let cat of modalSelectedSourceListItem?.metadata?.category">{{cat | medicalSourcesCategoryLookup}}</li>
</ul>
</ng-container>
</ng-container>
</div>
<div class="modal-footer">
<!-- <button (click)="sourceSyncHandler(modalSelectedSourceListItem.source)" type="button" class="btn btn-indigo">Sync</button>-->
<!-- <button (click)="connectHandler($event, modalSelectedSourceListItem.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>-->
<button type="button" (click)="connectHandler($event, modalSelectedSourceListItem)" class="btn btn-indigo">
<span *ngIf="status[modalSelectedSourceListItem?.metadata?.source_type] == 'authorize'" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Connect
</button>
<button (click)="modal.dismiss('Close click')" type="button" class="btn btn-outline-light">Close</button>
</div>
</ng-template>
<ng-template #isLoadingTemplate>
<div class="row">
<div class="col-12">

View File

@ -43,7 +43,7 @@ export class MedicalSourcesComponent implements OnInit {
availableSourceList: SourceListItem[] = []
searchTermUpdate = new BehaviorSubject<string>("");
status: { [name: string]: string } = {}
status: { [name: string]: undefined | "token" | "authorize" } = {}
//aggregation/filter data & limits
globalLimits: {
@ -68,8 +68,13 @@ export class MedicalSourcesComponent implements OnInit {
//source of truth for current state
//TODO: see if we can remove this without breaking search/filtering
filterForm = this.filterService.filterForm;
//modal
modalSelectedSourceListItem:SourceListItem = null;
modalCloseResult = '';
constructor(
private lighthouseApi: LighthouseService,
private fastenApi: FastenApiService,
@ -77,7 +82,7 @@ export class MedicalSourcesComponent implements OnInit {
private location: Location,
private toastService: ToastService,
private filterService: MedicalSourcesFilterService,
private modalService: NgbModal,
) { }
ngOnInit(): void {
@ -210,7 +215,7 @@ export class MedicalSourcesComponent implements OnInit {
}))
//change the current Page (but don't cause a new query)
if(wrapper.hits.hits.length == 0){
if(!wrapper?.hits || !wrapper?.hits || wrapper?.hits?.hits?.length == 0){
console.log("SCROLL_COMPLETE!@@@@@@@@")
this.resultLimits.scrollComplete = true;
} else {
@ -223,33 +228,33 @@ export class MedicalSourcesComponent implements OnInit {
// }))
if(wrapper.aggregations){
this.resultLimits.platformTypesBuckets = wrapper.aggregations.by_platform_type;
this.resultLimits.categoryBuckets = wrapper.aggregations.by_category;
var currentCategories = this.filterForm.get('categories').value;
this.resultLimits.categoryBuckets.buckets.forEach((bucketData) => {
if(!currentCategories.hasOwnProperty(bucketData.key)){
(this.filterForm.get('categories') as FormGroup).addControl(bucketData.key, new FormControl(false))
}
})
this.resultLimits.platformTypesBuckets = wrapper.aggregations.by_platform_type;
this.resultLimits.categoryBuckets = wrapper.aggregations.by_category;
//
// this.resultLimits.categoryBuckets.forEach((bucketData) => {
// if(!this.globalLimits.categories.some((category) => { return category.id === bucketData.key})){
// this.globalLimits.categories.push({
// id: bucketData.key,
// name: bucketData.key,
// group: 'custom'
// })
// }
// })
// const fileTypes = <FormGroup>this.filterForm.get('fileTypes');
// fileTypes.forEach((option: any) => {
// checkboxes.addControl(option.title, new FormControl(true));
// });
}
var currentCategories = this.filterForm.get('categories').value;
this.resultLimits.categoryBuckets.buckets.forEach((bucketData) => {
if(!currentCategories.hasOwnProperty(bucketData.key)){
(this.filterForm.get('categories') as FormGroup).addControl(bucketData.key, new FormControl(false))
}
})
//
// this.resultLimits.categoryBuckets.forEach((bucketData) => {
// if(!this.globalLimits.categories.some((category) => { return category.id === bucketData.key})){
// this.globalLimits.categories.push({
// id: bucketData.key,
// name: bucketData.key,
// group: 'custom'
// })
// }
// })
// const fileTypes = <FormGroup>this.filterForm.get('fileTypes');
// fileTypes.forEach((option: any) => {
// checkboxes.addControl(option.title, new FormControl(true));
// });
this.loading = false
},
error => {
@ -265,6 +270,10 @@ export class MedicalSourcesComponent implements OnInit {
}
public onScroll(): void {
this.querySources()
}
//OLD FUNCTIONS
//
//
@ -284,32 +293,49 @@ export class MedicalSourcesComponent implements OnInit {
// }))
// }
//
public onScroll(): void {
console.log("TODO: SCROLL, TRIGGER update")
this.querySources()
}
// /**
// * after pressing the logo (connectHandler button), this function will generate an authorize url for this source, and redirec the user.
// * after pressing the logo (connectModalHandler button), this function will display a modal with information about the source
// * @param $event
// * @param sourceType
// */
public connectHandler($event: MouseEvent, sourceType: string):void {
public connectModalHandler(contentModalRef, sourceListItem: SourceListItem) :void {
console.log("TODO: connect Handler")
// ($event.currentTarget as HTMLButtonElement).disabled = true;
// this.status[sourceType] = "authorize"
//
// this.lighthouseApi.getLighthouseSource(sourceType)
// .then(async (sourceMetadata: LighthouseSourceMetadata) => {
// console.log(sourceMetadata);
// let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
//
// console.log('authorize url:', authorizationUrl.toString());
// // redirect to lighthouse with uri's
// this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri)
//
// });
this.modalSelectedSourceListItem = sourceListItem
this.modalService.open(contentModalRef, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => {
this.modalSelectedSourceListItem = null
this.modalCloseResult = `Closed with: ${result}`;
}, (reason) => {
this.modalSelectedSourceListItem = null
});
}
// /**
// * after pressing the connect button in the Modal, this function will generate an authorize url for this source, and redirec the user.
// * @param $event
// * @param sourceType
// */
public connectHandler($event, sourceListItem: SourceListItem): void {
($event.currentTarget as HTMLButtonElement).disabled = true;
this.status[sourceListItem.metadata.source_type] = "authorize"
let sourceType = sourceListItem.metadata.source_type
this.lighthouseApi.getLighthouseSource(sourceType)
.then(async (sourceMetadata: LighthouseSourceMetadata) => {
console.log(sourceMetadata);
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
console.log('authorize url:', authorizationUrl.toString());
// redirect to lighthouse with uri's
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType, sourceMetadata.redirect_uri)
});
}
//
// /**
// * if the user is redirected to this page from the lighthouse, we'll need to process the "code" to retrieve the access token & refresh token.

View File

@ -105,11 +105,11 @@ export class LighthouseService {
}
//this is for providers that support CORS and PKCE (public client auth)
if(!lighthouseSource.confidential || lighthouseSource.code_challenge_methods_supported.length > 0){
if(!lighthouseSource.confidential || (lighthouseSource.code_challenge_methods_supported || []).length > 0){
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
const codeVerifier = Oauth.generateRandomCodeVerifier();
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported[0] || 'S256'
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported?.[0] || 'S256'
sourceStateInfo.code_verifier = codeVerifier
sourceStateInfo.code_challenge = codeChallenge

View File

@ -215,13 +215,20 @@ app-nlm-typeahead {
}
}
//Medical Source Page
.modal-header-media-image {
max-height: 50px;
max-width: 100px;
}
//Medical Source Filter
.category-label {
font-weight: 400
}
app-medical-sources-filter > .az-content-left-components {
overflow: hidden;
overflow-x: hidden;
-webkit-transition: width 0.2s ease-in-out;
-moz-transition: width 0.2s ease-in-out;
-o-transition: width 0.2s ease-in-out;