Skip to content

Commit

Permalink
Convert IPS viewer to svelte (#48)
Browse files Browse the repository at this point in the history
* Add viewer demo environment variable

* Move viewer resources to lib folder

* Refactor shared navigation components between layouts

* Implement viewer layout and logic in svelte components

* Limit global accordion body styling in IPSContent component to specific class

* Limit IPSContent global table styling to ips-section class

* Fix viewer loading animation

* Increase viewer section max height

* Re-add global modifier to loader styling
  • Loading branch information
daniellrgn authored Nov 7, 2024
1 parent 504cc6e commit 42c3338
Show file tree
Hide file tree
Showing 22 changed files with 3,424 additions and 229 deletions.
3 changes: 3 additions & 0 deletions default.env
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ VITE_VERSION_STRING=
# (Works best in Chrome)
# DEBUG=1

# Adds demo tab to IPS viewer
# VITE_SHOW_VIEWER_DEMO=1

###
### Client environment variables:
### Variables with VITE_ prefix will be available to the client
Expand Down
1 change: 1 addition & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface ImportMetaEnv {
readonly VITE_MEDITECH_BEARER_TOKEN: string
readonly VITE_API_BASE: string
readonly VITE_VIEWER_BASE: string
readonly VITE_SHOW_VIEWER_DEMO: boolean
readonly VITE_INTERMEDIATE_FHIR_SERVER_BASE: string
readonly VITE_VERSION_STRING: string
readonly DEV_SERVER_PORT: number
Expand Down
2 changes: 2 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export const VIEWER_BASE = new URL(
(import.meta.env.VITE_VIEWER_BASE ? import.meta.env.VITE_VIEWER_BASE : `/ips${dev ? '/index.html' : ''}`)+'#',
window.location.href
).toString();
export const SHOW_VIEWER_DEMO = import.meta.env.VITE_SHOW_VIEWER_DEMO;

export const PATIENT_IPS = {
'Dave deBronkart': 'https://fhir.ips-demo.dev.cirg.uw.edu/fhir/Patient/16501/$summary',
'Peter Kieth Jordan': 'https://terminz.azurewebsites.net/fhir/Patient/$summary?profile=http://hl7.org/fhir/uv/ips/StructureDefinition/Bundle-uv-ips&identifier=https://standards.digital.health.nz/ns/nhi-id|NNJ9186&_format=json'
Expand Down
36 changes: 36 additions & 0 deletions src/lib/layout/Banner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import { Row, Col, Image } from 'sveltestrap';
export let title = 'International Patient Summary';
</script>

<Row
style="
padding:0px 12px;
margin-left: 0px;
margin-right: 0px;
margin-bottom: 20px;
border-bottom: 1px solid rgb(204, 204, 204);"
class="d-flex justify-content-between align-items-center"
>
<Col style="max-width: 200px">
<Image
alt="WA Verify Logo"
width="200"
src="/img/waverifypluslogo.png"
style="align-self: center"
/>
</Col>
<Col>
<div
style="
vertical-align: middle;
font-size: 18px;
display: inline-block;
padding-left: 17px;
font-family: Verdana, sans-serif;
color: rgb(34, 72, 156);"
>
{title}
</div>
</Col>
</Row>
7 changes: 7 additions & 0 deletions src/lib/layout/navbar/HomeButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang='ts'>
import { NavItem, NavLink } from 'sveltestrap';
</script>

<NavItem>
<NavLink href="/home">Home</NavLink>
</NavItem>
31 changes: 31 additions & 0 deletions src/lib/layout/navbar/LanguageMenu.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle, Icon } from 'sveltestrap';
// import { locale } from 'svelte-i18n'; // TODO
import { getContext } from 'svelte';
import type { Writable, Readable } from 'svelte/store';
import type { Language } from '$lib/types';
let locale: Writable<string> = getContext('locale');
let locales: Readable<Record<string, Language>> = getContext('locales');
</script>

<Dropdown nav inNavbar size="sm" direction="down">
<DropdownToggle color="primary" nav caret>
<Icon name="globe2" />
{$locales[$locale].lang}
</DropdownToggle>
<DropdownMenu end style="height: 500px; overflow:auto">
{#if $locales}
{#each Object.values($locales) as loc}
<DropdownItem
style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden;"
on:click={() => {
$locale = loc.code;
}}
>
{`${loc.lang_en}${loc.lang_en !== loc.lang ? ' - ' + loc.lang : ''}`}
</DropdownItem>
{/each}
{/if}
</DropdownMenu>
</Dropdown>
6 changes: 6 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@ export interface DocumentReferencePOLST extends DocumentReference {
}

export type DocumentReferenceAD = DocumentReferencePOLST | DocumentReference;

export interface Language {
lang_en: string;
lang: string;
code: string;
}
198 changes: 198 additions & 0 deletions src/lib/viewer/Demo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<!-- Submit data view w/ input box, "try a sample" button, link to sample list, clear, and submit buttons -->
<!-- Simple checks view -->
<!-- Link to validation -->
<script lang="ts">
import {
Button,
Col,
FormGroup,
Icon,
Input,
Label,
Row
} from "sveltestrap";
import { onMount } from "svelte";
import type { Bundle } from "fhir/r4";
import IpsContent from "./IPSContent.svelte";
export let content: Bundle | undefined;
export let mode: string;
let demoContent: string;
onMount (() => {
if (!content) {
loadSample().then(submit);
}
});
$: {
if (!content) {
demoContent = "";
} else {
demoContent = JSON.stringify(content, null, 2);
}
}
let textInput: string;
$: textInput = demoContent;
let error: string[] = [];
let valid: boolean = true;
let invalid: boolean = false;
$: {
if (textInput) {
try {
let result = checks(JSON.parse(textInput));
if (result.errors) {
result.errors.forEach(element => {
setInputError(element);
});
} else {
clearInputErrors();
}
} catch (e: any) {
setInputError(e.message);
}
}
}
function setInputError(message: string) {
valid = false;
invalid = true;
error.push(message);
}
function clearInputErrors() {
valid = true;
invalid = false;
error = [];
}
async function loadSample() {
let sample = await fetch('/samples/sample.json').then(function(response) {
if (!response.ok) {
// make the promise be rejected if we didn't get a 2xx response
throw new Error("Unable to fetch IPS", {cause: response});
} else {
return response;
}
}).then(function(response) {
if (!response.ok) {
// make the promise be rejected if we didn't get a 2xx response
throw new Error("Unable to fetch IPS", {cause: response});
} else {
return response.text();
}
}).then((text) => {
return JSON.stringify(JSON.parse(text), null, 2);
}).catch(function (e) {
console.log("error", e);
});
if (sample) {
textInput = sample;
}
}
function clear() {
textInput = "";
}
let submitted = false;
function submit() {
try {
content = JSON.parse(textInput);
clearInputErrors();
submitted = true;
setTimeout(() => submitted = false, 2000);
} catch (e: any) {
setInputError(e.message);
}
}
interface ValidationResult {
display?: string,
entries?: number,
entriesColor?: string,
narrative?: string,
narrativeColor?: string
};
function checks(ips: Bundle) {
let composition = ips.entry?.[0];
let data = {
data: [] as ValidationResult[],
errors: [] as string[]
};
if (composition?.resource?.resourceType === "Composition" && composition.resource.section) {
let sections = {
allergies: false,
medications: false,
problems: false
};
for (let i = 0; i < composition.resource.section.length; i++) {
let section = composition.resource.section[i]
let newData = {} as ValidationResult;
newData.display = section.title;
if (section.code?.coding?.[0]?.code == "48765-2") sections.allergies = true;
if (section.code?.coding?.[0]?.code == "10160-0") sections.medications = true;
if (section.code?.coding?.[0]?.code == "11450-4") sections.problems = true;
if (section.entry) {
newData.entries = section.entry.length;
newData.entriesColor = "green";
} else {
newData.entries = 0;
newData.entriesColor = "red";
}
if (section.text && section.text.div) {
newData.narrative = ""
newData.narrativeColor = "green";
} else {
newData.narrative = ""
newData.narrativeColor = "red";
}
data.data.push(newData);
}
if (!sections.allergies) data.errors.push("Missing required allergies section");
if (!sections.medications) data.errors.push("Missing required medications section");
if (!sections.problems) data.errors.push("Missing required problems section");
}
return data;
}
</script>
<Row class="mx-1">
<h3>Submit Data</h3>
<p>This is for test data only. <span class="text-danger"><strong>Please do not submit PHI.</strong></span></p>
<FormGroup>
<Label>Paste your IPS JSON here:</Label>
<Input rows={8} type="textarea" bind:value={textInput} {valid} {invalid} feedback={error} class="pr-10"/>
</FormGroup>
</Row>
<Row class="mx-3" cols={{ sm: 2, xs: 1 }}>
<Col class="d-flex justify-content-start align-items-center">
<Button class="m-1" color="danger" on:click={clear}>Clear</Button>
<Button class="m-1" color="primary" on:click={submit}>Submit</Button>
{#if submitted}
<Icon name="check" class="text-success fs-5"/>
{/if}
</Col>
<Col class="d-flex justify-content-end align-items-center">
<Col class="m-1 justify-content-end align-items-center">
<Button color="success" on:click={loadSample} style="width:max-content">Try a Sample</Button>
</Col>
<Col class="d-flex flex-fill justify-content-start align-items-center">
<a href="https://github.com/jddamore/IPSviewer/tree/main/samples" class="m-1" target="_blank" rel="noreferrer">Repository of IPS Samples</a>
</Col>
</Col>
</Row>
{#if content}
<Row>
<IpsContent content={content} mode={mode} />
</Row>
{/if}

<style>
:global(textarea.form-control.is-valid, textarea.form-control.is-invalid) {
background-position: top calc(.375em + .1875rem) right 1.5rem !important;
}
</style>
Loading

0 comments on commit 42c3338

Please sign in to comment.