Timeline based Manual Entry form (#330)

This commit is contained in:
Jason Kulatunga 2023-12-03 18:40:16 -08:00 committed by GitHub
parent 7a77dcdcd6
commit d23af018e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
280 changed files with 5336 additions and 2828 deletions

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"strings" "strings"
"time" "time"
@ -39,6 +40,7 @@ func (gr *GormRepository) Close() error {
// User // User
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="User">
func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) error { func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) error {
if err := user.HashPassword(user.Password); err != nil { if err := user.HashPassword(user.Password); err != nil {
return err return err
@ -53,6 +55,17 @@ func (gr *GormRepository) CreateUser(ctx context.Context, user *models.User) err
if err != nil { if err != nil {
return err return err
} }
//create Fasten source credential for this user.
fastenUserCred := models.SourceCredential{
UserID: user.ID,
SourceType: sourcePkg.SourceTypeFasten,
}
fastenUserCredResp := gr.GormClient.Create(&fastenUserCred)
if fastenUserCredResp.Error != nil {
return fastenUserCredResp.Error
}
return nil return nil
} }
func (gr *GormRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) { func (gr *GormRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
@ -94,10 +107,13 @@ func (gr *GormRepository) GetCurrentUser(ctx context.Context) (*models.User, err
return &currentUser, nil return &currentUser, nil
} }
//</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Glossary // Glossary
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="Glossary">
func (gr *GormRepository) CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error { func (gr *GormRepository) CreateGlossaryEntry(ctx context.Context, glossaryEntry *models.Glossary) error {
record := gr.GormClient.WithContext(ctx).Create(glossaryEntry) record := gr.GormClient.WithContext(ctx).Create(glossaryEntry)
if record.Error != nil { if record.Error != nil {
@ -114,6 +130,8 @@ func (gr *GormRepository) GetGlossaryEntry(ctx context.Context, code string, cod
return &foundGlossaryEntry, result.Error return &foundGlossaryEntry, result.Error
} }
//</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Summary // Summary
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -180,6 +198,8 @@ func (gr *GormRepository) GetSummary(ctx context.Context) (*models.Summary, erro
// Resource // Resource
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="Resource">
// This function will create a new resource if it does not exist, or update an existing resource if it does exist. // This function will create a new resource if it does not exist, or update an existing resource if it does exist.
// It will also create associations between fhir resources // It will also create associations between fhir resources
// This function is called directly by fasten-sources // This function is called directly by fasten-sources
@ -210,12 +230,35 @@ func (gr *GormRepository) UpsertRawResource(ctx context.Context, sourceCredentia
//note: these associations are not reciprocal, (i.e. if Procedure references Location, Location may not reference Procedure) //note: these associations are not reciprocal, (i.e. if Procedure references Location, Location may not reference Procedure)
if rawResource.ReferencedResources != nil && len(rawResource.ReferencedResources) > 0 { if rawResource.ReferencedResources != nil && len(rawResource.ReferencedResources) > 0 {
for _, referencedResource := range rawResource.ReferencedResources { for _, referencedResource := range rawResource.ReferencedResources {
var relatedResource *models.ResourceBase
if strings.HasPrefix(referencedResource, sourcePkg.FASTENHEALTH_URN_PREFIX) {
gr.Logger.Infof("parsing external urn:fastenhealth-fhir reference: %v", referencedResource)
targetSourceId, targetResourceType, targetResourceId, err := sourcePkg.ParseReferenceUri(&referencedResource)
if err != nil {
gr.Logger.Warnf("could not parse urn:fastenhealth-fhir reference: %v", referencedResource)
continue
}
err = gr.UpsertRawResourceAssociation(
ctx,
source.ID.String(),
wrappedResourceModel.SourceResourceType,
wrappedResourceModel.SourceResourceID,
targetSourceId,
targetResourceType,
targetResourceId,
)
if err != nil {
return false, err
}
} else {
parts := strings.Split(referencedResource, "/") parts := strings.Split(referencedResource, "/")
if len(parts) != 2 { if len(parts) != 2 {
continue continue
} }
relatedResource = &models.ResourceBase{
relatedResource := &models.ResourceBase{
OriginBase: models.OriginBase{ OriginBase: models.OriginBase{
SourceID: source.ID, SourceID: source.ID,
SourceResourceType: parts[0], SourceResourceType: parts[0],
@ -237,11 +280,50 @@ func (gr *GormRepository) UpsertRawResource(ctx context.Context, sourceCredentia
} }
} }
} }
}
return gr.UpsertResource(ctx, wrappedResourceModel) return gr.UpsertResource(ctx, wrappedResourceModel)
} }
func (gr *GormRepository) UpsertRawResourceAssociation(
ctx context.Context,
sourceId string,
sourceResourceType string,
sourceResourceId string,
targetSourceId string,
targetResourceType string,
targetResourceId string,
) error {
if sourceId == targetSourceId && sourceResourceType == targetResourceType && sourceResourceId == targetResourceId {
gr.Logger.Warnf("cannot create self-referential association, ignoring")
return nil
}
var sourceCredential *models.SourceCredential
var targetSourceCredential *models.SourceCredential
var err error
if sourceId == targetSourceId {
sourceCredential, err = gr.GetSource(ctx, sourceId)
if err != nil {
return err
}
targetSourceCredential = sourceCredential
} else {
sourceCredential, err = gr.GetSource(ctx, sourceId)
if err != nil {
return err
}
targetSourceCredential, err = gr.GetSource(ctx, targetSourceId)
if err != nil {
return err
}
}
//SECURITY: sourceCredential and targetSourceCredential are guaranteed to be owned by the same user, and will be confirmed within the addAssociation function
return gr.AddResourceAssociation(ctx, sourceCredential, sourceResourceType, sourceResourceId, targetSourceCredential, targetResourceType, targetResourceId)
}
// UpsertResource // UpsertResource
// this method will upsert a resource, however it will not create associations. // this method will upsert a resource, however it will not create associations.
// UPSERT operation // UPSERT operation
@ -448,10 +530,14 @@ func (gr *GormRepository) GetPatientForSources(ctx context.Context) ([]models.Re
return wrappedResourceModels, results.Error return wrappedResourceModels, results.Error
} }
//</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Associations // Resource Associations
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//<editor-fold desc="Resource Associations">
// verifyAssociationPermission ensure that the sources are "owned" by the same user, and that the user is the current user // verifyAssociationPermission ensure that the sources are "owned" by the same user, and that the user is the current user
func (gr *GormRepository) verifyAssociationPermission(ctx context.Context, sourceUserID uuid.UUID, relatedSourceUserID uuid.UUID) error { func (gr *GormRepository) verifyAssociationPermission(ctx context.Context, sourceUserID uuid.UUID, relatedSourceUserID uuid.UUID) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx) currentUser, currentUserErr := gr.GetCurrentUser(ctx)
@ -542,11 +628,14 @@ func (gr *GormRepository) FindResourceAssociationsByTypeAndId(ctx context.Contex
ResourceBaseSourceID: source.ID, ResourceBaseSourceID: source.ID,
ResourceBaseSourceResourceType: resourceType, ResourceBaseSourceResourceType: resourceType,
ResourceBaseSourceResourceID: resourceId, ResourceBaseSourceResourceID: resourceId,
RelatedResourceUserID: currentUser.ID,
}). }).
Find(&relatedResources) Find(&relatedResources)
return relatedResources, result.Error return relatedResources, result.Error
} }
//</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Composition (Grouping) // Resource Composition (Grouping)
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -565,6 +654,8 @@ func (gr *GormRepository) FindResourceAssociationsByTypeAndId(ctx context.Contex
// - add AddResourceAssociation for all resources linked to the Composition resource // - add AddResourceAssociation for all resources linked to the Composition resource
// - store the Composition resource // - store the Composition resource
// TODO: determine if we should be using a List Resource instead of a Composition resource // TODO: determine if we should be using a List Resource instead of a Composition resource
//
// Deprecated: This method has been deprecated. It has been replaced in favor of Fasten SourceCredential & associations
func (gr *GormRepository) AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error { func (gr *GormRepository) AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx) currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil { if currentUserErr != nil {
@ -718,6 +809,8 @@ func (gr *GormRepository) AddResourceComposition(ctx context.Context, compositio
// SourceCredential // SourceCredential
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//<editor-fold desc="SourceCredential">
func (gr *GormRepository) CreateSource(ctx context.Context, sourceCreds *models.SourceCredential) error { func (gr *GormRepository) CreateSource(ctx context.Context, sourceCreds *models.SourceCredential) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx) currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil { if currentUserErr != nil {
@ -846,10 +939,10 @@ func (gr *GormRepository) GetSourceSummary(ctx context.Context, sourceId string)
Table(patientTableName). Table(patientTableName).
First(&wrappedPatientResourceModel) First(&wrappedPatientResourceModel)
if patientResults.Error != nil { //some sources may not have a patient resource (including the Fasten source)
return nil, patientResults.Error if patientResults.Error == nil {
}
sourceSummary.Patient = &wrappedPatientResourceModel sourceSummary.Patient = &wrappedPatientResourceModel
}
return sourceSummary, nil return sourceSummary, nil
} }
@ -925,10 +1018,13 @@ func (gr *GormRepository) DeleteSource(ctx context.Context, sourceId string) (in
return rowsEffected, results.Error return rowsEffected, results.Error
} }
//</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Background Job // Background Job
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <editor-fold desc="Background Job & Checkpoints">
func (gr *GormRepository) CreateBackgroundJob(ctx context.Context, backgroundJob *models.BackgroundJob) error { func (gr *GormRepository) CreateBackgroundJob(ctx context.Context, backgroundJob *models.BackgroundJob) error {
currentUser, currentUserErr := gr.GetCurrentUser(ctx) currentUser, currentUserErr := gr.GetCurrentUser(ctx)
if currentUserErr != nil { if currentUserErr != nil {
@ -1117,6 +1213,8 @@ func (gr *GormRepository) CancelAllLockedBackgroundJobsAndFail() error {
} }
//</editor-fold>
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utilities // Utilities
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -1,8 +1,11 @@
package database package database
import ( import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models" "github.com/fastenhealth/fasten-onprem/backend/pkg/models"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database" databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/go-gormigrate/gormigrate/v2" "github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -14,7 +17,7 @@ func (gr *GormRepository) Migrate() error {
gormMigrateOptions := gormigrate.DefaultOptions gormMigrateOptions := gormigrate.DefaultOptions
gormMigrateOptions.UseTransaction = true gormMigrateOptions.UseTransaction = true
//use echo $(date '+%Y%m%d%H%M%S') to generate new ID's //use "echo $(date '+%Y%m%d%H%M%S')" to generate new ID's
m := gormigrate.New(gr.GormClient, gormMigrateOptions, []*gormigrate.Migration{ m := gormigrate.New(gr.GormClient, gormMigrateOptions, []*gormigrate.Migration{
{ {
ID: "20231017112246", // base database models //TODO: figure out how to version these correctly (SourceCredential is complicated) ID: "20231017112246", // base database models //TODO: figure out how to version these correctly (SourceCredential is complicated)
@ -37,6 +40,31 @@ func (gr *GormRepository) Migrate() error {
return databaseModel.Migrate(tx) return databaseModel.Migrate(tx)
}, },
}, },
{
ID: "20231201122541", // Adding Fasten Source Credential for each user
Migrate: func(tx *gorm.DB) error {
users := []models.User{}
results := tx.Find(&users)
if results.Error != nil {
return results.Error
}
for _, user := range users {
tx.Logger.Info(context.Background(), fmt.Sprintf("Creating Fasten Source Credential for user: %s", user.ID))
fastenUserCred := models.SourceCredential{
UserID: user.ID,
SourceType: sourcePkg.SourceTypeFasten,
}
fastenUserCredCreateResp := tx.Create(&fastenUserCred)
if fastenUserCredCreateResp.Error != nil {
tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred creating Fasten Source Credential for user: %s", user.ID))
return fastenUserCredCreateResp.Error
}
}
return nil
},
},
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {

View File

@ -1021,7 +1021,7 @@ func (suite *RepositoryTestSuite) TestGetSummary() {
{"count": int64(16), "resource_type": "Procedure"}, {"count": int64(16), "resource_type": "Procedure"},
}, sourceSummary.ResourceTypeCounts) }, sourceSummary.ResourceTypeCounts)
require.Equal(suite.T(), 2, len(sourceSummary.Sources)) require.Equal(suite.T(), 3, len(sourceSummary.Sources))
require.Equal(suite.T(), 2, len(sourceSummary.Patients)) require.Equal(suite.T(), 2, len(sourceSummary.Patients))
} }

View File

@ -29,6 +29,8 @@ type DatabaseRepository interface {
RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error RemoveResourceAssociation(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string, relatedSource *models.SourceCredential, relatedResourceType string, relatedResourceId string) error
FindResourceAssociationsByTypeAndId(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string) ([]models.RelatedResource, error) FindResourceAssociationsByTypeAndId(ctx context.Context, source *models.SourceCredential, resourceType string, resourceId string) ([]models.RelatedResource, error)
GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error) GetFlattenedResourceGraph(ctx context.Context, graphType pkg.ResourceGraphType, options models.ResourceGraphOptions) (map[string][]*models.ResourceBase, error)
// Deprecated:This method has been deprecated. It has been replaced in favor of Fasten SourceCredential & associations
AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error AddResourceComposition(ctx context.Context, compositionTitle string, resources []*models.ResourceBase) error
//UpsertProfile(context.Context, *models.Profile) error //UpsertProfile(context.Context, *models.Profile) error
//UpsertOrganziation(context.Context, *models.Organization) error //UpsertOrganziation(context.Context, *models.Organization) error
@ -55,6 +57,15 @@ type DatabaseRepository interface {
PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error
//used by fasten-sources Clients //used by fasten-sources Clients
UpsertRawResource(ctx context.Context, sourceCredentials sourcePkg.SourceCredential, rawResource sourcePkg.RawResourceFhir) (bool, error)
BackgroundJobCheckpoint(ctx context.Context, checkpointData map[string]interface{}, errorData map[string]interface{}) BackgroundJobCheckpoint(ctx context.Context, checkpointData map[string]interface{}, errorData map[string]interface{})
UpsertRawResource(ctx context.Context, sourceCredentials sourcePkg.SourceCredential, rawResource sourcePkg.RawResourceFhir) (bool, error)
UpsertRawResourceAssociation(
ctx context.Context,
sourceId string,
sourceResourceType string,
sourceResourceId string,
targetSourceId string,
targetResourceType string,
targetResourceId string,
) error
} }

View File

@ -69,6 +69,9 @@ type SourceCredential struct {
func (s *SourceCredential) GetSourceType() sourcesPkg.SourceType { func (s *SourceCredential) GetSourceType() sourcesPkg.SourceType {
return s.SourceType return s.SourceType
} }
func (s *SourceCredential) GetSourceId() string {
return s.ID.String()
}
func (s *SourceCredential) GetClientId() string { func (s *SourceCredential) GetClientId() string {
return s.ClientId return s.ClientId

View File

@ -17,14 +17,65 @@ import (
"time" "time"
) )
// BackgroundJobSyncResources is a background job that syncs all FHIR resource for a given source // This function is used to sync resources from a source (via a callback function). The BackgroundJobSyncResourcesWrapper contains the logic for registering the background job tracking the sync.
func BackgroundJobSyncResources(
parentContext context.Context,
logger *logrus.Entry,
databaseRepo database.DatabaseRepository,
sourceCred *models.SourceCredential,
) (sourceModels.UpsertSummary, error) {
return BackgroundJobSyncResourcesWrapper(
parentContext,
logger,
databaseRepo,
sourceCred,
func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) {
// after creating the client, we should do a bulk import
sourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), _sourceCred.SourceType, _backgroundJobContext, _logger, _sourceCred)
if err != nil {
resultErr := fmt.Errorf("an error occurred while initializing hub client using source credential: %w", err)
_logger.Errorln(resultErr)
return nil, sourceModels.UpsertSummary{}, resultErr
}
summary, err := sourceClient.SyncAll(_databaseRepo)
if err != nil {
resultErr := fmt.Errorf("an error occurred while bulk importing resources from source: %w", err)
_logger.Errorln(resultErr)
return sourceClient, summary, resultErr
}
return sourceClient, summary, nil
})
}
// BackgroundJobSyncResourcesWrapper is a background job that syncs all FHIR resource for a given source
// It is a blocking function that will return only when the sync is complete or has failed // It is a blocking function that will return only when the sync is complete or has failed
// It will create a background job and associate it with the source // It will create a background job and associate it with the source
// It will also update the access token and refresh token if they have been updated // It will also update the access token and refresh token if they have been updated
// It will return the sync summary and error if any // It will return the sync summary and error if any
//
// It's a wrapper function that takes a callback function as an argument.
// The callback function is the actual sync operation that will be run in the background (regular source or manual source)
//
// TODO: run in background thread, or use https://gobyexample.com/tickers // TODO: run in background thread, or use https://gobyexample.com/tickers
// TODO: use goroutine to truely run in the background (how will that work with DatabaseRepository, is that thread safe?) Mutex needed? // TODO: use goroutine to truely run in the background (how will that work with DatabaseRepository, is that thread safe?) Mutex needed?
func BackgroundJobSyncResources(parentContext context.Context, logger *logrus.Entry, databaseRepo database.DatabaseRepository, sourceCred *models.SourceCredential) (sourceModels.UpsertSummary, error) { func BackgroundJobSyncResourcesWrapper(
parentContext context.Context,
logger *logrus.Entry,
databaseRepo database.DatabaseRepository,
sourceCred *models.SourceCredential,
callbackFn func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error),
) (sourceModels.UpsertSummary, error) {
var resultErr error var resultErr error
var backgroundJob *models.BackgroundJob var backgroundJob *models.BackgroundJob
@ -54,14 +105,6 @@ func BackgroundJobSyncResources(parentContext context.Context, logger *logrus.En
//we can safely ignore this error, because we'll be updating the status of the background job again later //we can safely ignore this error, because we'll be updating the status of the background job again later
} }
// after creating the client, we should do a bulk import
sourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourceCred.SourceType, backgroundJobContext, logger, sourceCred)
if err != nil {
resultErr = fmt.Errorf("an error occurred while initializing hub client using source credential: %w", err)
logger.Errorln(resultErr)
return sourceModels.UpsertSummary{}, resultErr
}
// BEGIN FINALIZER // BEGIN FINALIZER
defer func() { defer func() {
//finalizer function - update the sync status to completed (or failed depending on the error status) //finalizer function - update the sync status to completed (or failed depending on the error status)
@ -124,10 +167,11 @@ func BackgroundJobSyncResources(parentContext context.Context, logger *logrus.En
}() }()
// END FINALIZER // END FINALIZER
summary, err := sourceClient.SyncAll(databaseRepo) var sourceClient sourceModels.SourceClient
if err != nil { var summary sourceModels.UpsertSummary
resultErr = fmt.Errorf("an error occurred while bulk importing resources from source: %w", err) sourceClient, summary, resultErr = callbackFn(backgroundJobContext, logger, databaseRepo, sourceCred)
logger.Errorln(resultErr) if resultErr != nil {
logger.Errorln("An error occurred while syncing resources, ignoring", resultErr)
return summary, resultErr return summary, resultErr
} }

View File

@ -94,6 +94,7 @@ func GetResourceFhir(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "data": wrappedResourceModel}) c.JSON(http.StatusOK, gin.H{"success": true, "data": wrappedResourceModel})
} }
// deprecated - using Manual Resource Wizard instead
func CreateResourceComposition(c *gin.Context) { func CreateResourceComposition(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry) logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)

View File

@ -0,0 +1,95 @@
package handler
import (
"context"
"fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-sources/clients/factory"
sourceModels "github.com/fastenhealth/fasten-sources/clients/models"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"net/http"
)
// mimics functionality in CreateManualSource
func CreateRelatedResources(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
eventBus := c.MustGet(pkg.ContextKeyTypeEventBusServer).(event_bus.Interface)
// store the bundle file locally
bundleFile, err := storeFileLocally(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//step 2: find a reference to the Fasten source for this user
sourceCredentials, err := databaseRepo.GetSources(c)
var fastenSourceCredential *models.SourceCredential
for _, sourceCredential := range sourceCredentials {
if sourceCredential.SourceType == sourcePkg.SourceTypeFasten {
fastenSourceCredential = &sourceCredential
break
}
}
if fastenSourceCredential == nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not find Fasten source for this user"})
return
}
summary, err := BackgroundJobSyncResourcesWrapper(
c,
logger,
databaseRepo,
fastenSourceCredential,
func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) {
//step 3: create a "fasten" client, which we can use to parse resources to add to the database
fastenSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeFasten, _backgroundJobContext, _logger, _sourceCred)
if err != nil {
resultErr := fmt.Errorf("could not create Fasten source client")
_logger.Errorln(resultErr)
return fastenSourceClient, sourceModels.UpsertSummary{}, resultErr
}
//step 4: parse the resources from the bundle
summary, err := fastenSourceClient.SyncAllBundle(_databaseRepo, bundleFile, sourcePkg.FhirVersion401)
if err != nil {
resultErr := fmt.Errorf("an error occurred while processing bundle: %v", err)
_logger.Errorln(resultErr)
return fastenSourceClient, summary, resultErr
}
return fastenSourceClient, summary, nil
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
//step 7 notify the event bus of the new resources
currentUser, _ := databaseRepo.GetCurrentUser(c)
err = eventBus.PublishMessage(
models.NewEventSourceComplete(
currentUser.ID.String(),
fastenSourceCredential.ID.String(),
),
)
if err != nil {
logger.Warnf("ignoring: an error occurred while publishing sync complete event: %v", err)
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": summary, "source": fastenSourceCredential})
}

View File

@ -1,18 +1,21 @@
package handler package handler
import ( import (
"context"
"fmt" "fmt"
"github.com/fastenhealth/fasten-onprem/backend/pkg" "github.com/fastenhealth/fasten-onprem/backend/pkg"
"github.com/fastenhealth/fasten-onprem/backend/pkg/database" "github.com/fastenhealth/fasten-onprem/backend/pkg/database"
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus" "github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/models" "github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/fastenhealth/fasten-sources/clients/factory" "github.com/fastenhealth/fasten-sources/clients/factory"
sourceModels "github.com/fastenhealth/fasten-sources/clients/models"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg" sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
) )
func CreateReconnectSource(c *gin.Context) { func CreateReconnectSource(c *gin.Context) {
@ -122,30 +125,17 @@ func SourceSync(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "source": sourceCred, "data": summary}) c.JSON(http.StatusOK, gin.H{"success": true, "source": sourceCred, "data": summary})
} }
// mimics functionality in CreateRelatedResources
// mimics functionality in SourceSync
func CreateManualSource(c *gin.Context) { func CreateManualSource(c *gin.Context) {
logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry) logger := c.MustGet(pkg.ContextKeyTypeLogger).(*logrus.Entry)
databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository) databaseRepo := c.MustGet(pkg.ContextKeyTypeDatabase).(database.DatabaseRepository)
eventBus := c.MustGet(pkg.ContextKeyTypeEventBusServer).(event_bus.Interface) eventBus := c.MustGet(pkg.ContextKeyTypeEventBusServer).(event_bus.Interface)
// single file // store the bundle file locally
file, err := c.FormFile("file") bundleFile, err := storeFileLocally(c)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not extract file from form"}) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return
}
fmt.Printf("Uploaded filename: %s", file.Filename)
// create a temporary file to store this uploaded file
bundleFile, err := ioutil.TempFile("", file.Filename)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not create temp file"})
return
}
// Upload the file to specific bundleFile.
err = c.SaveUploadedFile(file, bundleFile.Name())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not save temp file"})
return return
} }
@ -178,23 +168,40 @@ func CreateManualSource(c *gin.Context) {
return return
} }
manualSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeManual, c, logger, &manualSourceCredential) summary, err := BackgroundJobSyncResourcesWrapper(
c,
logger,
databaseRepo,
&manualSourceCredential,
func(
_backgroundJobContext context.Context,
_logger *logrus.Entry,
_databaseRepo database.DatabaseRepository,
_sourceCred *models.SourceCredential,
) (sourceModels.SourceClient, sourceModels.UpsertSummary, error) {
manualSourceClient, err := factory.GetSourceClient(sourcePkg.GetFastenLighthouseEnv(), sourcePkg.SourceTypeManual, _backgroundJobContext, _logger, _sourceCred)
if err != nil { if err != nil {
logger.Errorln("An error occurred while initializing hub client using manual source with credential", err) resultErr := fmt.Errorf("an error occurred while initializing hub client using manual source with credential: %w", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false}) logger.Errorln(resultErr)
return return manualSourceClient, sourceModels.UpsertSummary{}, resultErr
} }
summary, err := manualSourceClient.SyncAllBundle(databaseRepo, bundleFile, bundleType) summary, err := manualSourceClient.SyncAllBundle(_databaseRepo, bundleFile, bundleType)
if err != nil {
resultErr := fmt.Errorf("an error occurred while processing bundle: %w", err)
logger.Errorln(resultErr)
return manualSourceClient, sourceModels.UpsertSummary{}, resultErr
}
return manualSourceClient, summary, nil
})
if err != nil { if err != nil {
logger.Errorln("An error occurred while processing bundle", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()})
return return
} }
//publish event //publish event
currentUser, _ := databaseRepo.GetCurrentUser(c) currentUser, _ := databaseRepo.GetCurrentUser(c)
err = eventBus.PublishMessage( err = eventBus.PublishMessage(
models.NewEventSourceComplete( models.NewEventSourceComplete(
currentUser.ID.String(), currentUser.ID.String(),
@ -260,3 +267,29 @@ func DeleteSource(c *gin.Context) {
} }
c.JSON(http.StatusOK, gin.H{"success": true, "data": rowsEffected}) c.JSON(http.StatusOK, gin.H{"success": true, "data": rowsEffected})
} }
// Helpers
func storeFileLocally(c *gin.Context) (*os.File, error) {
// single file
file, err := c.FormFile("file")
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not extract file from form"})
return nil, fmt.Errorf("could not extract file from form")
}
fmt.Printf("Uploaded filename: %s", file.Filename)
// create a temporary file to store this uploaded file
bundleFile, err := ioutil.TempFile("", file.Filename)
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not create temp file"})
return nil, fmt.Errorf("could not create temp file")
}
// Upload the file to specific bundleFile.
err = c.SaveUploadedFile(file, bundleFile.Name())
if err != nil {
//c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "could not save temp file"})
return nil, fmt.Errorf("could not save temp file")
}
return bundleFile, nil
}

View File

@ -74,7 +74,9 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
secure.GET("/resource/fhir", handler.ListResourceFhir) secure.GET("/resource/fhir", handler.ListResourceFhir)
secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph) secure.POST("/resource/graph/:graphType", handler.GetResourceFhirGraph)
secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir) secure.GET("/resource/fhir/:sourceId/:resourceId", handler.GetResourceFhir)
secure.POST("/resource/composition", handler.CreateResourceComposition) secure.POST("/resource/composition", handler.CreateResourceComposition)
secure.POST("/resource/related", handler.CreateRelatedResources)
secure.GET("/dashboards", handler.GetDashboard) secure.GET("/dashboards", handler.GetDashboard)
secure.POST("/dashboards", handler.AddDashboardLocation) secure.POST("/dashboards", handler.AddDashboardLocation)

View File

@ -51,6 +51,7 @@
"ngx-highlightjs": "^7.0.1", "ngx-highlightjs": "^7.0.1",
"ngx-infinite-scroll": "^14.0.0", "ngx-infinite-scroll": "^14.0.0",
"ngx-moment": "^6.0.2", "ngx-moment": "^6.0.2",
"parse-full-name": "^1.2.6",
"rxjs": "~6.5.4", "rxjs": "~6.5.4",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"uuid": "^9.0.0", "uuid": "^9.0.0",

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import {NavigationEnd, Router} from '@angular/router'; import {NavigationEnd, Router} from '@angular/router';
import {Observable, of} from 'rxjs'; import {Observable, of} from 'rxjs';
import {ToastService} from './services/toast.service'; import {ToastService} from './services/toast.service';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -15,7 +16,11 @@ export class AppComponent implements OnInit {
showHeader:boolean = false; showHeader:boolean = false;
showFooter:boolean = true; showFooter:boolean = true;
constructor(private router: Router, private toastService: ToastService) {} constructor(
private router: Router,
private toastService: ToastService,
private modalService: NgbModal
) {}
ngOnInit() { ngOnInit() {
@ -25,17 +30,21 @@ export class AppComponent implements OnInit {
document.querySelector('body').appendChild(navbarBackdrop); document.querySelector('body').appendChild(navbarBackdrop);
//determine if we should show the header //determine if we should show the header
this.router.events.subscribe(event => this.modifyHeader(event)); this.router.events.subscribe(event => this.routerEvent(event));
} }
modifyHeader(event) { routerEvent(event) {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
//modify header
if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop')) { if (event.url?.startsWith('/auth') || event.url?.startsWith('/desktop')) {
this.showHeader = false; this.showHeader = false;
} else { } else {
// console.log("NU") // console.log("NU")
this.showHeader = true; this.showHeader = true;
} }
// close all open modals when route change
this.modalService.dismissAll();
} }
} }
} }

View File

@ -38,6 +38,8 @@ import { ExploreComponent } from './pages/explore/explore.component';
import {DirectivesModule} from './directives/directives.module'; import {DirectivesModule} from './directives/directives.module';
import { DesktopCallbackComponent } from './pages/desktop-callback/desktop-callback.component'; import { DesktopCallbackComponent } from './pages/desktop-callback/desktop-callback.component';
import { BackgroundJobsComponent } from './pages/background-jobs/background-jobs.component'; import { BackgroundJobsComponent } from './pages/background-jobs/background-jobs.component';
import {FhirCardModule} from './components/fhir-card/fhir-card.module';
import {FhirDatatableModule} from './components/fhir-datatable/fhir-datatable.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -64,6 +66,8 @@ import { BackgroundJobsComponent } from './pages/background-jobs/background-jobs
BrowserModule, BrowserModule,
FontAwesomeModule, FontAwesomeModule,
SharedModule, SharedModule,
FhirCardModule,
FhirDatatableModule,
AppRoutingModule, AppRoutingModule,
HttpClientModule, HttpClientModule,
NgbModule, NgbModule,

View File

@ -3,7 +3,7 @@ import {BadgeComponent} from "./badge.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BadgeComponent> = { const meta: Meta<BadgeComponent> = {
title: 'Fhir/Common/Badge', title: 'Fhir Card/Common/Badge',
component: BadgeComponent, component: BadgeComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -5,7 +5,7 @@ import {FastenDisplayModel} from "../../../../../lib/models/fasten/fasten-displa
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<TableComponent> = { const meta: Meta<TableComponent> = {
title: 'Fhir/Common/Table', title: 'Fhir Card/Common/Table',
component: TableComponent, component: TableComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -8,7 +8,7 @@ import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BinaryTextComponent> = { const meta: Meta<BinaryTextComponent> = {
title: 'Fhir/Datatypes/BinaryText', title: 'Fhir Card/Datatypes/BinaryText',
component: BinaryTextComponent, component: BinaryTextComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -6,7 +6,7 @@ import {DicomComponent} from "./dicom.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<DicomComponent> = { const meta: Meta<DicomComponent> = {
title: 'Fhir/Datatypes/Dicom', title: 'Fhir Card/Datatypes/Dicom',
component: DicomComponent, component: DicomComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -8,7 +8,7 @@ import {HtmlComponent} from "./html.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<HtmlComponent> = { const meta: Meta<HtmlComponent> = {
title: 'Fhir/Datatypes/Html', title: 'Fhir Card/Datatypes/Html',
component: HtmlComponent, component: HtmlComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -6,7 +6,7 @@ import {ImgComponent} from "./img.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<ImgComponent> = { const meta: Meta<ImgComponent> = {
title: 'Fhir/Datatypes/Img', title: 'Fhir Card/Datatypes/Img',
component: ImgComponent, component: ImgComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -0,0 +1,45 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/binary/exampleMarkdown.json";
import {BinaryModel} from "../../../../../lib/models/resources/binary-model";
import {MarkdownComponent} from "./markdown.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<MarkdownComponent> = {
title: 'Fhir Card/Datatypes/Markdown',
component: MarkdownComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: MarkdownComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
},
};
export default meta;
type Story = StoryObj<MarkdownComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
let aiDisplayModel1 = new BinaryModel(R4Example1Json, fhirVersions.R4)
aiDisplayModel1.source_id = '123-456-789'
aiDisplayModel1.source_resource_id = '123-456-789'
export const R4Example1: Story = {
args: {
displayModel: aiDisplayModel1
}
};

View File

@ -6,7 +6,7 @@ import {PdfComponent} from "./pdf.component";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<PdfComponent> = { const meta: Meta<PdfComponent> = {
title: 'Fhir/Datatypes/Pdf', title: 'Fhir Card/Datatypes/Pdf',
component: PdfComponent, component: PdfComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({

View File

@ -0,0 +1,106 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {BadgeComponent} from './common/badge/badge.component';
import {TableComponent} from './common/table/table.component';
import {BinaryTextComponent} from './datatypes/binary-text/binary-text.component';
import {CodableConceptComponent} from './datatypes/codable-concept/codable-concept.component';
import {CodingComponent} from './datatypes/coding/coding.component';
import {DicomComponent} from './datatypes/dicom/dicom.component';
import {HtmlComponent} from './datatypes/html/html.component';
import {ImgComponent} from './datatypes/img/img.component';
import {MarkdownComponent} from './datatypes/markdown/markdown.component';
import {PdfComponent} from './datatypes/pdf/pdf.component';
import {AllergyIntoleranceComponent} from './resources/allergy-intolerance/allergy-intolerance.component';
import {BinaryComponent} from './resources/binary/binary.component';
import {DiagnosticReportComponent} from './resources/diagnostic-report/diagnostic-report.component';
import {DocumentReferenceComponent} from './resources/document-reference/document-reference.component';
import {FallbackComponent} from './resources/fallback/fallback.component';
import {ImmunizationComponent} from './resources/immunization/immunization.component';
import {LocationComponent} from './resources/location/location.component';
import {MediaComponent} from './resources/media/media.component';
import {MedicationComponent} from './resources/medication/medication.component';
import {MedicationRequestComponent} from './resources/medication-request/medication-request.component';
import {ObservationComponent} from './resources/observation/observation.component';
import {OrganizationComponent} from './resources/organization/organization.component';
import {PractitionerComponent} from './resources/practitioner/practitioner.component';
import {ProcedureComponent} from './resources/procedure/procedure.component';
import {FhirCardComponent} from './fhir-card/fhir-card.component';
import {FhirCardOutletDirective} from './fhir-card/fhir-card-outlet.directive';
import { EncounterComponent } from './resources/encounter/encounter.component';
@NgModule({
imports: [
//common
CommonModule,
BadgeComponent,
//datatypes
TableComponent,
BinaryTextComponent,
CodableConceptComponent,
CodingComponent,
DicomComponent,
HtmlComponent,
ImgComponent,
MarkdownComponent,
PdfComponent,
//resources
AllergyIntoleranceComponent,
BinaryComponent,
DiagnosticReportComponent,
DocumentReferenceComponent,
EncounterComponent,
FallbackComponent,
ImmunizationComponent,
LocationComponent,
MediaComponent,
MedicationComponent,
MedicationRequestComponent,
ObservationComponent,
OrganizationComponent,
PractitionerComponent,
ProcedureComponent,
],
//TODO: every component in here should be migrated to a standalone component
declarations: [
FhirCardComponent,
FhirCardOutletDirective,
],
exports:[
//common
BadgeComponent,
TableComponent,
//datatypes
BinaryTextComponent,
CodableConceptComponent,
CodingComponent,
DicomComponent,
HtmlComponent,
ImgComponent,
MarkdownComponent,
PdfComponent,
//resources
AllergyIntoleranceComponent,
BinaryComponent,
DiagnosticReportComponent,
DocumentReferenceComponent,
EncounterComponent,
FallbackComponent,
ImmunizationComponent,
LocationComponent,
MediaComponent,
MedicationComponent,
MedicationRequestComponent,
ObservationComponent,
OrganizationComponent,
PractitionerComponent,
ProcedureComponent,
FhirCardComponent,
FhirCardOutletDirective,
]
})
export class FhirCardModule { }

View File

@ -1,9 +1,10 @@
import {FastenDisplayModel} from '../../../../lib/models/fasten/fasten-display-model'; import {FastenDisplayModel} from '../../../../lib/models/fasten/fasten-display-model';
//all Fhir Resource components must implement this Interface //all Fhir Resource components must implement this Interface
export interface FhirResourceComponentInterface { export interface FhirCardComponentInterface {
displayModel: FastenDisplayModel; displayModel: FastenDisplayModel;
showDetails: boolean; showDetails: boolean;
isCollapsed: boolean;
//these are used to populate the description of the resource. May not be available for all resources //these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string; resourceCode?: string;

View File

@ -1,4 +1,4 @@
import { FhirResourceOutletDirective } from './fhir-resource-outlet.directive'; import { FhirCardOutletDirective } from './fhir-card-outlet.directive';
import { import {
ComponentFactory, ComponentRef, ComponentFactory, ComponentRef,
@ -66,11 +66,11 @@ class TestViewContainerRef extends ViewContainerRef {
} }
describe('FhirResourceOutletDirective', () => { describe('FhirCardOutletDirective', () => {
it('should create an instance', () => { it('should create an instance', () => {
const directive = new FhirResourceOutletDirective(new TestViewContainerRef()); const directive = new FhirCardOutletDirective(new TestViewContainerRef());
expect(directive).toBeTruthy(); expect(directive).toBeTruthy();
}); });
}); });

View File

@ -1,9 +1,9 @@
import {Directive, ViewContainerRef} from '@angular/core'; import {Directive, ViewContainerRef} from '@angular/core';
@Directive({ @Directive({
selector: '[resourceListOutlet]' selector: '[fhirCardOutlet]'
}) })
export class ResourceListOutletDirective { export class FhirCardOutletDirective {
constructor(public viewContainerRef: ViewContainerRef) { } constructor(public viewContainerRef: ViewContainerRef) { }

View File

@ -0,0 +1 @@
<ng-template fhirCardOutlet></ng-template>

View File

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

View File

@ -9,12 +9,12 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import {BinaryModel} from '../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../lib/models/resources/binary-model';
import {FhirResourceOutletDirective} from './fhir-resource-outlet.directive'; import {FhirCardOutletDirective} from './fhir-card-outlet.directive';
import {ResourceType} from '../../../../lib/models/constants'; import {ResourceType} from '../../../../lib/models/constants';
import {FallbackComponent} from '../resources/fallback/fallback.component'; import {FallbackComponent} from '../resources/fallback/fallback.component';
import {BinaryComponent} from '../resources/binary/binary.component'; import {BinaryComponent} from '../resources/binary/binary.component';
import {FhirResourceComponentInterface} from './fhir-resource-component-interface'; import {FhirCardComponentInterface} from './fhir-card-component-interface';
import {ImmunizationComponent} from '../resources/immunization/immunization.component'; import {ImmunizationComponent} from '../resources/immunization/immunization.component';
import {AllergyIntoleranceComponent} from '../resources/allergy-intolerance/allergy-intolerance.component'; import {AllergyIntoleranceComponent} from '../resources/allergy-intolerance/allergy-intolerance.component';
import {MedicationComponent} from '../resources/medication/medication.component'; import {MedicationComponent} from '../resources/medication/medication.component';
@ -28,21 +28,23 @@ import {MediaComponent} from '../resources/media/media.component';
import {LocationComponent} from '../resources/location/location.component'; import {LocationComponent} from '../resources/location/location.component';
import {OrganizationComponent} from '../resources/organization/organization.component'; import {OrganizationComponent} from '../resources/organization/organization.component';
import {ObservationComponent} from '../resources/observation/observation.component'; import {ObservationComponent} from '../resources/observation/observation.component';
import {EncounterComponent} from '../resources/encounter/encounter.component';
@Component({ @Component({
selector: 'fhir-resource', selector: 'fhir-card',
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
templateUrl: './fhir-resource.component.html', templateUrl: './fhir-card.component.html',
styleUrls: ['./fhir-resource.component.scss'] styleUrls: ['./fhir-card.component.scss']
}) })
export class FhirResourceComponent implements OnInit, OnChanges { export class FhirCardComponent implements OnInit, OnChanges {
@Input() displayModel: FastenDisplayModel @Input() displayModel: FastenDisplayModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
//location to dynamically load the displayModel //location to dynamically load the displayModel
@ViewChild(FhirResourceOutletDirective, {static: true}) fhirResourceOutlet!: FhirResourceOutletDirective; @ViewChild(FhirCardOutletDirective, {static: true}) fhirCardOutlet!: FhirCardOutletDirective;
constructor() { } constructor() { }
@ -55,21 +57,22 @@ export class FhirResourceComponent implements OnInit, OnChanges {
loadComponent() { loadComponent() {
//clear the current outlet //clear the current outlet
const viewContainerRef = this.fhirResourceOutlet.viewContainerRef; const viewContainerRef = this.fhirCardOutlet.viewContainerRef;
viewContainerRef.clear(); viewContainerRef.clear();
let componentType = this.typeLookup(this.displayModel?.source_resource_type) let componentType = this.typeLookup(this.displayModel?.source_resource_type)
if(componentType != null){ if(componentType != null){
console.log("Attempting to create fhir display component", this.displayModel, componentType) console.log("Attempting to create fhir display component", this.displayModel, componentType)
const componentRef = viewContainerRef.createComponent<FhirResourceComponentInterface>(componentType); const componentRef = viewContainerRef.createComponent<FhirCardComponentInterface>(componentType);
componentRef.instance.displayModel = this.displayModel; componentRef.instance.displayModel = this.displayModel;
componentRef.instance.showDetails = this.showDetails; componentRef.instance.showDetails = this.showDetails;
componentRef.instance.isCollapsed = this.isCollapsed;
componentRef.instance.markForCheck() componentRef.instance.markForCheck()
} }
} }
typeLookup(resourceType: ResourceType): Type<FhirResourceComponentInterface> { typeLookup(resourceType: ResourceType): Type<FhirCardComponentInterface> {
if(!resourceType){ if(!resourceType){
//dont try to render anything if the resourceType isnt set. //dont try to render anything if the resourceType isnt set.
return null return null
@ -114,9 +117,9 @@ export class FhirResourceComponent implements OnInit, OnChanges {
case "DocumentReference": { case "DocumentReference": {
return DocumentReferenceComponent; return DocumentReferenceComponent;
} }
// case "Encounter": { case "Encounter": {
// return ListEncounterComponent; return EncounterComponent;
// } }
// case "Goal": { // case "Goal": {
// return ListGoalComponent; // return ListGoalComponent;
// } // }

View File

@ -1,5 +1,5 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface'; import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router, RouterModule} from '@angular/router'; import {Router, RouterModule} from '@angular/router';
import {AllergyIntoleranceModel} from '../../../../../lib/models/resources/allergy-intolerance-model'; import {AllergyIntoleranceModel} from '../../../../../lib/models/resources/allergy-intolerance-model';
@ -15,11 +15,10 @@ import {TableComponent} from "../../common/table/table.component";
templateUrl: './allergy-intolerance.component.html', templateUrl: './allergy-intolerance.component.html',
styleUrls: ['./allergy-intolerance.component.scss'] styleUrls: ['./allergy-intolerance.component.scss']
}) })
export class AllergyIntoleranceComponent implements OnInit, FhirResourceComponentInterface { export class AllergyIntoleranceComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: AllergyIntoleranceModel @Input() displayModel: AllergyIntoleranceModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
isCollapsed: boolean = false
tableData: TableRowItem[] = [] tableData: TableRowItem[] = []

View File

@ -10,7 +10,7 @@ import R4Example3Json from "../../../../../lib/fixtures/r4/resources/allergyInto
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<AllergyIntoleranceComponent> = { const meta: Meta<AllergyIntoleranceComponent> = {
title: 'Fhir/AllergyIntolerance', title: 'Fhir Card/AllergyIntolerance',
component: AllergyIntoleranceComponent, component: AllergyIntoleranceComponent,
decorators: [ decorators: [
// moduleMetadata({ // moduleMetadata({
@ -51,8 +51,8 @@ export const R4Example1: Story = {
}; };
let aiDisplayModel2 = new AllergyIntoleranceModel(R4Example2Json, fhirVersions.R4) let aiDisplayModel2 = new AllergyIntoleranceModel(R4Example2Json, fhirVersions.R4)
aiDisplayModel1.source_id = '123-456-789' aiDisplayModel2.source_id = '123-456-789'
aiDisplayModel1.source_resource_id = '123-456-789' aiDisplayModel2.source_resource_id = '123-456-789'
export const R4Example2: Story = { export const R4Example2: Story = {
args: { args: {
displayModel: aiDisplayModel2 displayModel: aiDisplayModel2
@ -60,8 +60,8 @@ export const R4Example2: Story = {
}; };
let aiDisplayModel3 = new AllergyIntoleranceModel(R4Example3Json, fhirVersions.R4) let aiDisplayModel3 = new AllergyIntoleranceModel(R4Example3Json, fhirVersions.R4)
aiDisplayModel1.source_id = '123-456-789' aiDisplayModel3.source_id = '123-456-789'
aiDisplayModel1.source_resource_id = '123-456-789' aiDisplayModel3.source_resource_id = '123-456-789'
export const R4Example3: Story = { export const R4Example3: Story = {
args: { args: {
displayModel: aiDisplayModel3 displayModel: aiDisplayModel3

View File

@ -1,6 +1,6 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface'; import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {Router, RouterModule} from '@angular/router'; import {Router, RouterModule} from '@angular/router';
import {AttachmentModel} from '../../../../../lib/models/datatypes/attachment-model'; import {AttachmentModel} from '../../../../../lib/models/datatypes/attachment-model';
import {FastenApiService} from '../../../../services/fasten-api.service'; import {FastenApiService} from '../../../../services/fasten-api.service';
@ -37,11 +37,12 @@ import {AuthService} from "../../../../services/auth.service";
templateUrl: './binary.component.html', templateUrl: './binary.component.html',
styleUrls: ['./binary.component.scss'] styleUrls: ['./binary.component.scss']
}) })
export class BinaryComponent implements OnInit, FhirResourceComponentInterface { export class BinaryComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: BinaryModel @Input() displayModel: BinaryModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
@Input() attachmentSourceId: string @Input() attachmentSourceId: string
@Input() attachmentModel: AttachmentModel //can only have attachmentModel or binaryModel, not both. @Input() attachmentModel: AttachmentModel //can only have attachmentModel or binaryModel, not both.
@Input() isCollapsed: boolean = false
loading: boolean = false loading: boolean = false
constructor(public changeRef: ChangeDetectorRef, public router: Router, public fastenApi: FastenApiService) {} constructor(public changeRef: ChangeDetectorRef, public router: Router, public fastenApi: FastenApiService) {}

View File

@ -18,7 +18,7 @@ import {HTTP_CLIENT_TOKEN} from '../../../../dependency-injection';
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<BinaryComponent> = { const meta: Meta<BinaryComponent> = {
title: 'Fhir/Binary', title: 'Fhir Card/Binary',
component: BinaryComponent, component: BinaryComponent,
decorators: [ decorators: [
moduleMetadata({ moduleMetadata({

View File

@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DiagnosticReportComponent } from './diagnostic-report.component'; import { DiagnosticReportComponent } from './diagnostic-report.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap'; import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {RouterTestingModule} from '@angular/router/testing';
describe('DiagnosticReportComponent', () => { describe('DiagnosticReportComponent', () => {
let component: DiagnosticReportComponent; let component: DiagnosticReportComponent;
@ -9,8 +10,7 @@ describe('DiagnosticReportComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ DiagnosticReportComponent ], imports: [NgbCollapseModule, DiagnosticReportComponent, RouterTestingModule]
imports: [NgbCollapseModule]
}) })
.compileComponents(); .compileComponents();

View File

@ -1,22 +1,31 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface'; import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router, RouterModule} from '@angular/router';
import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model'; import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {BinaryComponent} from '../binary/binary.component';
import {GlossaryLookupComponent} from '../../../glossary-lookup/glossary-lookup.component';
@Component({ @Component({
selector: 'app-diagnostic-report', standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule, BinaryComponent, GlossaryLookupComponent],
selector: 'fhir-diagnostic-report',
templateUrl: './diagnostic-report.component.html', templateUrl: './diagnostic-report.component.html',
styleUrls: ['./diagnostic-report.component.scss'] styleUrls: ['./diagnostic-report.component.scss']
}) })
export class DiagnosticReportComponent implements OnInit, FhirResourceComponentInterface { export class DiagnosticReportComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: DiagnosticReportModel @Input() displayModel: DiagnosticReportModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
//these are used to populate the description of the resource. May not be available for all resources //these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string; resourceCode?: string;
resourceCodeSystem?: string; resourceCodeSystem?: string;
isCollapsed: boolean = false
tableData: TableRowItem[] = [] tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) {} constructor(public changeRef: ChangeDetectorRef, public router: Router) {}

View File

@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DocumentReferenceComponent } from './document-reference.component'; import { DocumentReferenceComponent } from './document-reference.component';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap'; import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {RouterTestingModule} from '@angular/router/testing';
describe('DocumentReferenceComponent', () => { describe('DocumentReferenceComponent', () => {
let component: DocumentReferenceComponent; let component: DocumentReferenceComponent;
@ -9,8 +10,8 @@ describe('DocumentReferenceComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [NgbCollapseModule], imports: [NgbCollapseModule, DocumentReferenceComponent, RouterTestingModule],
declarations: [ DocumentReferenceComponent ] declarations: [ ]
}) })
.compileComponents(); .compileComponents();

View File

@ -1,19 +1,27 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model'; import {DiagnosticReportModel} from '../../../../../lib/models/resources/diagnostic-report-model';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item'; import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {Router} from '@angular/router'; import {Router, RouterModule} from '@angular/router';
import {DocumentReferenceModel} from '../../../../../lib/models/resources/document-reference-model'; import {DocumentReferenceModel} from '../../../../../lib/models/resources/document-reference-model';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface'; import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {BinaryComponent} from '../binary/binary.component';
import {GlossaryLookupComponent} from '../../../glossary-lookup/glossary-lookup.component';
@Component({ @Component({
selector: 'app-document-reference', standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, RouterModule, BinaryComponent],
selector: 'fhir-document-reference',
templateUrl: './document-reference.component.html', templateUrl: './document-reference.component.html',
styleUrls: ['./document-reference.component.scss'] styleUrls: ['./document-reference.component.scss']
}) })
export class DocumentReferenceComponent implements OnInit, FhirResourceComponentInterface { export class DocumentReferenceComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: DocumentReferenceModel @Input() displayModel: DocumentReferenceModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
isCollapsed: boolean = false @Input() isCollapsed: boolean = false
tableData: TableRowItem[] = [] tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) {} constructor(public changeRef: ChangeDetectorRef, public router: Router) {}

View File

@ -0,0 +1,19 @@
<div class="card card-fhir-resource">
<div class="card-header" (click)="isCollapsed = ! isCollapsed">
<div>
<h6 class="card-title">{{displayModel?.sort_title || displayModel?.code?.text}}</h6>
<p class="card-text tx-gray-400" *ngIf="displayModel?.period_start"><strong>Start date</strong> {{displayModel?.period_start | date}}</p>
</div>
<!-- <div class="btn-group">-->
<!-- <button class="btn active">Day</button>-->
<!-- <button class="btn">Week</button>-->
<!-- <button class="btn">Month</button>-->
<!-- </div>-->
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" class="card-body">
<fhir-ui-table [displayModel]="displayModel" [tableData]="tableData"></fhir-ui-table>
</div>
<div *ngIf="showDetails" class="card-footer">
<a class="float-right" routerLink="/explore/{{displayModel?.source_id}}/resource/{{displayModel?.source_resource_id}}">details</a>
</div>
</div>

View File

@ -0,0 +1,24 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { EncounterComponent } from './encounter.component';
import {RouterTestingModule} from '@angular/router/testing';
describe('EncounterComponent', () => {
let component: EncounterComponent;
let fixture: ComponentFixture<EncounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ EncounterComponent, RouterTestingModule ]
})
.compileComponents();
fixture = TestBed.createComponent(EncounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {BadgeComponent} from '../../common/badge/badge.component';
import {TableComponent} from '../../common/table/table.component';
import {GlossaryLookupComponent} from '../../../glossary-lookup/glossary-lookup.component';
import {Router, RouterModule} from '@angular/router';
import {TableRowItem, TableRowItemDataType} from '../../common/table/table-row-item';
import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {EncounterModel} from '../../../../../lib/models/resources/encounter-model';
@Component({
standalone: true,
imports: [NgbCollapseModule, CommonModule, BadgeComponent, TableComponent, GlossaryLookupComponent, RouterModule],
selector: 'fhir-encounter',
templateUrl: './encounter.component.html',
styleUrls: ['./encounter.component.scss']
})
export class EncounterComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: EncounterModel | null
@Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
//these are used to populate the description of the resource. May not be available for all resources
resourceCode?: string;
resourceCodeSystem?: string;
tableData: TableRowItem[] = []
constructor(public changeRef: ChangeDetectorRef, public router: Router) { }
ngOnInit(): void {
this.tableData = [
{
label: 'Type',
data: this.displayModel?.encounter_type?.[0],
data_type: TableRowItemDataType.CodableConcept,
enabled: !!this.displayModel?.encounter_type?.[0],
},
{
label: 'Location',
data: this.displayModel?.location_display,
enabled: !!this.displayModel?.location_display,
},
];
}
markForCheck(){
this.changeRef.markForCheck()
}
}

View File

@ -0,0 +1,70 @@
import type { Meta, StoryObj } from '@storybook/angular';
import {EncounterComponent} from "./encounter.component";
import {EncounterModel} from "../../../../../lib/models/resources/encounter-model";
import {fhirVersions} from "../../../../../lib/models/constants";
import R4Example1Json from "../../../../../lib/fixtures/r4/resources/encounter/example1.json";
import R4Example2Json from "../../../../../lib/fixtures/r4/resources/encounter/example2.json";
import R4Example3Json from "../../../../../lib/fixtures/r4/resources/encounter/example3.json";
// More on how to set up stories at: https://storybook.js.org/docs/angular/writing-stories/introduction
const meta: Meta<EncounterComponent> = {
title: 'Fhir Card/Encounter',
component: EncounterComponent,
decorators: [
// moduleMetadata({
// imports: [AppModule]
// })
// applicationConfig({
// providers: [importProvidersFrom(AppModule)],
// }),
],
tags: ['autodocs'],
render: (args: EncounterComponent) => ({
props: {
backgroundColor: null,
...args,
},
}),
argTypes: {
displayModel: {
control: 'object',
},
showDetails: {
control: 'boolean',
}
},
};
export default meta;
type Story = StoryObj<EncounterComponent>;
// More on writing stories with args: https://storybook.js.org/docs/angular/writing-stories/args
let encounterDisplayModel1 = new EncounterModel(R4Example1Json, fhirVersions.R4)
encounterDisplayModel1.source_id = '123-456-789'
encounterDisplayModel1.source_resource_id = '123-456-789'
export const R4Example1: Story = {
args: {
displayModel: encounterDisplayModel1
}
};
let encounterDisplayModel2 = new EncounterModel(R4Example2Json, fhirVersions.R4)
encounterDisplayModel2.source_id = '123-456-789'
encounterDisplayModel2.source_resource_id = '123-456-789'
export const R4Example2: Story = {
args: {
displayModel: encounterDisplayModel2
}
};
let encounterDisplayModel3 = new EncounterModel(R4Example3Json, fhirVersions.R4)
encounterDisplayModel3.source_id = '123-456-789'
encounterDisplayModel3.source_resource_id = '123-456-789'
export const R4Example3: Story = {
args: {
displayModel: encounterDisplayModel3
}
};

View File

@ -8,7 +8,7 @@ describe('FallbackComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [ FallbackComponent ] imports: [ FallbackComponent ]
}) })
.compileComponents(); .compileComponents();

View File

@ -1,16 +1,22 @@
import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core'; import {ChangeDetectorRef, Component, Input, OnInit} from '@angular/core';
import {BinaryModel} from '../../../../../lib/models/resources/binary-model'; import {BinaryModel} from '../../../../../lib/models/resources/binary-model';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {FhirResourceComponentInterface} from '../../fhir-resource/fhir-resource-component-interface'; import {FhirCardComponentInterface} from '../../fhir-card/fhir-card-component-interface';
import {NgbCollapseModule} from '@ng-bootstrap/ng-bootstrap';
import {CommonModule} from '@angular/common';
import {HighlightModule} from 'ngx-highlightjs';
@Component({ @Component({
standalone: true,
imports: [NgbCollapseModule, HighlightModule, CommonModule],
selector: 'fhir-fallback', selector: 'fhir-fallback',
templateUrl: './fallback.component.html', templateUrl: './fallback.component.html',
styleUrls: ['./fallback.component.scss'] styleUrls: ['./fallback.component.scss']
}) })
export class FallbackComponent implements OnInit, FhirResourceComponentInterface { export class FallbackComponent implements OnInit, FhirCardComponentInterface {
@Input() displayModel: BinaryModel @Input() displayModel: BinaryModel
@Input() showDetails: boolean = true @Input() showDetails: boolean = true
@Input() isCollapsed: boolean = false
constructor(public changeRef: ChangeDetectorRef, public router: Router) {} constructor(public changeRef: ChangeDetectorRef, public router: Router) {}

Some files were not shown because too many files have changed in this diff Show More