Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to typing #14

Merged
merged 9 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ module.exports = {
"**/*.test.jsx"
],
env: {
jest: true
"jest": true,
"node": true
},
"extends": [
"eslint:recommended",
Expand Down
3 changes: 3 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import $ from 'jquery';
global.$ = $;

jest.setTimeout(10000);
if (process.env.CI) {
jest.setTimeout(30000); // Initial loading of profiles takes a lot longer on GH Actions
}

// Mock functions
window.scrollTo = jest.fn(); // unimplemented in jsdom
Expand Down
2 changes: 1 addition & 1 deletion src/domain-resource/value-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface ValueEditorProps {
onEditCancel: (e?: React.SyntheticEvent) => void,
hasFocus: boolean,
required: boolean,
shortName: any,
shortName: string,
}
class ValueEditor extends React.Component<ValueEditorProps, Record<string, never>> {
ESC_KEY: number;
Expand Down
64 changes: 33 additions & 31 deletions src/freezer-js.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,31 @@ declare module 'freezer-js' {
freezeInstances: boolean,
}

export type EventDict<E> = E extends {[K in keyof E]: (...args: any[]) => any} ? {
export type ExtractTypeOfFN<FN> = FN extends FreezerNode<infer T, infer E> ? T : never;

export type EventDict<E> = E extends {[K in keyof E]: (...args: any[]) => unknown} ? {
[K in keyof E]: E[K]
} : never;

// FreezerEvents shortened to FE because it will be repeated a lot!
export type FE<T, E extends EventDict<any>> = E & {
export type FE<T, E> = E & {
"update": (state: T, prevState: T) => void,
"beforeAll": <K extends keyof E>(eventName: K, ...args: Parameters<E[K]>) => void,
"afterAll": <K extends keyof E>(eventName: K, ...args: Parameters<E[K]>) => void,
"beforeAll": <K extends keyof E>(eventName: K, ...args: Parameters<E extends EventDict<E> ? E[K] : never>) => void,
"afterAll": <K extends keyof E>(eventName: K, ...args: Parameters<E extends EventDict<E> ? E[K] : never>) => void,
};

export type FreezerListener<E extends EventDict<any>> = {
on: <K extends keyof E>(eventName: K, cb: E[K]) => FreezerListener<E>;
once: <K extends keyof E>(eventName: K, cb: E[K]) => FreezerListener<E>;
off: <K extends keyof E>(eventName: K, cb: E[K]) => FreezerListener<E>;
emit: <K extends keyof E>(eventName: K, ...args: Parameters<E[K]>) => ReturnType<E[K]>;
trigger: <K extends keyof E>(eventName: K, ...args: Parameters<E[K]>) => ReturnType<E[K]>; // deprecated
export type FreezerListener<T, E> = {
on: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<T, E>;
once: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<T, E>;
off: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<T, E>;
emit: <K extends keyof FE<T, E>>(eventName: K, ...args: Parameters<E extends EventDict<E> ? FE<T, E>[K] : never>) => ReturnType<E extends EventDict<E> ? FE<T, E>[K] : never>;
trigger: <K extends keyof FE<T, E>>(eventName: K, ...args: Parameters<E extends EventDict<E> ? FE<T, E>[K] : never>) => ReturnType<E extends EventDict<E> ? FE<T, E>[K] : never>; // deprecated
};

export type FreezerNode<T, E extends EventDict<any>> = T extends Array<infer ArrayType> ? FreezerArray<ArrayType, E> : T extends string | number | boolean | null | undefined ? T : FreezerObject<T, E>;
export type FreezerNode<T, E> = T extends Array<infer ArrayType> ? FreezerArray<ArrayType, E> : T extends string | number | boolean | null | undefined ? T : FreezerObject<T, E>;

type FreezerCommon<T, E extends EventDict<any>> = FreezerListener<E> & {
getListener(): FreezerListener<E>,
type FreezerCommon<T, E> = FreezerListener<T, E> & {
getListener(): FreezerListener<T, E>,
now(): void,
pivot(): FreezerNodePivoted<T, E, T>,
reset(a: FreezerNode<T, E>): FreezerNode<T, E>,
Expand All @@ -38,7 +40,7 @@ declare module 'freezer-js' {
transact(): void,
}

type FreezerArray<T, E extends EventDict<any>> = FreezerCommon<T, E> & {
type FreezerArray<T, E> = FreezerCommon<T, E> & {
append(a: T[]): FreezerArray<T, E>,
pop(): T | undefined,
prepend(): FreezerArray<T, E>,
Expand All @@ -58,17 +60,17 @@ declare module 'freezer-js' {
: K
}[keyof T], undefined>

type FreezerObject<T, E extends EventDict<any>> = FreezerCommon<T, E> & {
type FreezerObject<T, E> = FreezerCommon<T, E> & {
remove(a: OptionalPropertyOf<T>): FreezerObject<T, E>,
remove(a: OptionalPropertyOf<T>[]): FreezerObject<T, E>,
} & {
[K in keyof T]-?: FreezerNode<T[K], E>
};

// Definitions repeated for pivots
export type FreezerNodePivoted<PivotType, E extends EventDict<any>, T> = T extends Array<infer Type> ? FreezerArrayPivoted<PivotType, E, Type> : T extends string | number | boolean | null | undefined ? T : FreezerObjectPivoted<PivotType, E, T>;
export type FreezerNodePivoted<PivotType, E, T> = T extends Array<infer ArrayType> ? FreezerArrayPivoted<PivotType, E, ArrayType> : T extends string | number | boolean | null | undefined ? T : FreezerObjectPivoted<PivotType, E, T>;

type FreezerCommonPivoted<PivotType, E extends EventDict<any>, T> = FreezerListener<E> & {
type FreezerCommonPivoted<PivotType, E, T> = FreezerListener<T, E> & {
getListener(): void,
now(): void,
pivot(): FreezerNodePivoted<T, E, T>,
Expand All @@ -80,7 +82,7 @@ declare module 'freezer-js' {
transact(): void,
}

type FreezerArrayPivoted<PivotType, E extends EventDict<any>, T> = FreezerCommonPivoted<PivotType, E, T> & {
type FreezerArrayPivoted<PivotType, E, T> = FreezerCommonPivoted<PivotType, E, T> & {
append(): FreezerNodePivoted<PivotType, E, PivotType>,
pop(): T | undefined,
prepend(): FreezerNodePivoted<PivotType, E, PivotType>,
Expand All @@ -93,32 +95,32 @@ declare module 'freezer-js' {
unshift(): FreezerNodePivoted<PivotType, E, PivotType>,
} & FreezerNodePivoted<PivotType, E, T>[];

type FreezerObjectPivoted<PivotType, E extends EventDict<any>, T> = FreezerCommonPivoted<PivotType, E, T> & {
type FreezerObjectPivoted<PivotType, E, T> = FreezerCommonPivoted<PivotType, E, T> & {
// remove(a: string): FreezerObject<Omit<T,a>>,
remove(a: string): FreezerNodePivoted<PivotType, E, PivotType>,
remove(a: string[]): FreezerNodePivoted<PivotType, E, PivotType>,
} & {
[K in keyof T]-?: FreezerNodePivoted<PivotType, E, T[K]>
};

export class Freezer<T, E extends EventDict<any>>{
export class Freezer<T, E>{
constructor(a: T, b?: FreezerOptions);

get() : FreezerNode<T, FE<T, E>>;
set(state: T) : FreezerNode<T, FE<T, E>>;
set(state: FreezerNode<T, FE<T, E>>) : FreezerNode<T, FE<T, E>>;
get() : FreezerNode<T, E>;
set(state: T) : FreezerNode<T, E>;
set(state: FreezerNode<T, E>) : FreezerNode<T, E>;
// set() : FreezerCommon<T, FE<T, E>>;

getEventHub() : FreezerListener<FE<T, E>>;
getEventHub() : FreezerListener<T, E>;

getData() : FreezerCommon<T, FE<T, E>>;
setData() : FreezerCommon<T, FE<T, E>>;
getData() : FreezerCommon<T, E>;
setData() : FreezerCommon<T, E>;

on: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<FE<T, E>>;
once: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<FE<T, E>>;
off: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<FE<T, E>>;
emit: <K extends keyof FE<T, E>>(eventName: K, ...args: Parameters<FE<T, E>[K]>) => ReturnType<FE<T, E>[K]>;
trigger: <K extends keyof FE<T, E>>(eventName: K, ...args: Parameters<FE<T, E>[K]>) => ReturnType<FE<T, E>[K]>; // deprecated
on: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<T, E>;
once: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<T, E>;
off: <K extends keyof FE<T, E>>(eventName: K, cb: FE<T, E>[K]) => FreezerListener<T, E>;
emit: <K extends keyof FE<T, E>>(eventName: K, ...args: Parameters<E extends EventDict<E> ? FE<T, E>[K] : never>) => ReturnType<E extends EventDict<E> ? FE<T, E>[K] : never>;
trigger: <K extends keyof FE<T, E>>(eventName: K, ...args: Parameters<E extends EventDict<E> ? FE<T, E>[K] : never>) => ReturnType<E extends EventDict<E> ? FE<T, E>[K] : never>; // deprecated

// reset(newData:any):void;

Expand Down
45 changes: 26 additions & 19 deletions src/helpers/sage-utils.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
import * as cql from 'cql-execution';
import { Library } from 'fhir/r4';

export function getCqlExecutionLibraryFromInputLibraryResource(resource: any) {
if (resource.content && Array.isArray(resource.content)) {
for (const content of resource.content) {
if (content.contentType == "application/elm+json") {
if (content.data) {
try {
const elm = JSON.parse(window.atob(content.data));
const parsedLib = getCqlExecutionLibraryFromJsonElm(elm);
if (parsedLib) {
return {
library: parsedLib,
url: resource.url
};
export function getCqlExecutionLibraryFromInputLibraryResource(resource: unknown) {
if (resource && typeof resource == 'object') {
const resourceToTest = resource as Partial<Library>; // Assume Partial type so that we're forced to check each property
if (!resourceToTest.url) {
return null; // Libraries are useless when building a PD if we can't reference them by URL
}
if (Array.isArray(resourceToTest.content)) {
for (const content of resourceToTest.content) {
if (content.contentType == "application/elm+json") {
if (content.data) {
try {
const elm = JSON.parse(window.atob(content.data));
const parsedLib = getCqlExecutionLibraryFromJsonElm(elm);
if (parsedLib) {
return {
library: parsedLib,
url: resourceToTest.url
};
}
}
catch(error) {
console.log("Could not parse encoded JSON ELM in resource:", resource);
return null;
}
}
catch(error) {
console.log("Could not parse encoded JSON ELM in resource:", resource);
return null;
}
}
}
console.log("No JSON ELM found in resource:", resource);
}
console.log("No JSON ELM found in resource:", resource);
}
return null;
}

export function getCqlExecutionLibraryFromJsonElm(elm: any) {
export function getCqlExecutionLibraryFromJsonElm(elm: unknown) {
try {
const parsedLib = new cql.Library(elm);
console.log('parsed library into:', parsedLib);
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/schema-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type SageNode = {
type: ElementDefinitionType,
level?: number,
sliceName: string,
short: any,
short: string,
isRequired: boolean,
profile: string, // This profile must be able to resolve `schemaPath`
binding?: any,
Expand Down
5 changes: 2 additions & 3 deletions src/simplified/baseCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export const BaseCard = (props: BaseCardProps) => {
let headerPadding = {};
if (props.title == "") headerPadding = {padding:"7px"};
const resourceType = friendlyToFhir(props.header);
const isActivity = resourceType?.includes("Activity");

return (
<CSSTransition
Expand All @@ -48,8 +47,8 @@ export const BaseCard = (props: BaseCardProps) => {
bg={props.bsBg}
text={props.bsText as Color}
border={props.bsBorder}
onClick={(e: any) => {
if (e.target.tagName !== "svg" && e.target.tagName !== "path" && props.clickable) {
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (e.target instanceof Element && e.target.tagName !== "svg" && e.target.tagName !== "path" && props.clickable) {
setShow(false);
if (State.get().bundle?.resources.length) {
State.emit("save_changes_to_bundle_json");
Expand Down
77 changes: 43 additions & 34 deletions src/simplified/collection.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,61 @@
import {useState, useEffect} from "react";
import {Folder} from"./folder";
import State from "../state";
import { useState, useEffect } from "react";
import { Folder } from "./folder";
import State, { SageReactions } from "../state";
import * as SchemaUtils from "../helpers/schema-utils"

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {faDownload, faCaretLeft} from '@fortawesome/pro-solid-svg-icons';
import { faDownload, faCaretLeft } from '@fortawesome/pro-solid-svg-icons';
import { SageNodeInitialized } from "../helpers/schema-utils";


const Collection = () => {

const resources = State.get().bundle?.resources ?? [];

return (
<div style={{marginTop:"50px"}}>
<div style={{ marginTop: "50px" }}>
<div className="row">
<h3 className="col-lg-10 col-md-9" style={{color:"#2a6b92"}}><b>Saved Cards</b></h3>
<button className="navigate-reverse col-lg-2 col-md-3"
onClick={() => State.get().set("ui", {status:"cards"})}>
<FontAwesomeIcon icon={faCaretLeft} />
&nbsp;New Card
</button>
<button className="navigate-reverse col-lg-2 col-md-2"
onClick={() => State.get().set("ui", {status:"export"})}>
<FontAwesomeIcon icon={faDownload} />
&nbsp;Export as FHIR Bundle
</button>
<h3 className="col-lg-10 col-md-9" style={{ color: "#2a6b92" }}><b>Saved Cards</b></h3>
<button className="navigate-reverse col-lg-2 col-md-3"
onClick={() => State.get().set("ui", { status: "cards" })}>
<FontAwesomeIcon icon={faCaretLeft} />
&nbsp;New Card
</button>
<button className="navigate-reverse col-lg-2 col-md-2"
onClick={() => State.get().set("ui", { status: "export" })}>
<FontAwesomeIcon icon={faDownload} />
&nbsp;Export as FHIR Bundle
</button>
</div>
<div className="row box">
{
resources.map(
(resource, i) => {
if (i % 2 == 1) return; // Every other resource would be a plandef
const actTitleNode = SchemaUtils.getChildOfNode(resource, "title");
const planTitleNode = SchemaUtils.getChildOfNode(resources[i+1], "title");
const firstExpression: string | undefined = SchemaUtils.getChildOfNodePath(resources[i+1], ["action", "condition", "expression", "expression"])?.value;
resources.reduce(
function (accumulator: any[], currentValue, currentIndex, array) {
if (currentIndex % 2 === 0)
accumulator.push(array.slice(currentIndex, currentIndex + 2));
return accumulator;
}, []).map((pair, i) => {
const actNode = pair[0];
const planDefNode = pair[1];
const actTitleNode = SchemaUtils.getChildOfNode(actNode, "title");
const planTitleNode = SchemaUtils.getChildOfNode(planDefNode, "title");
const actDescNode = SchemaUtils.getChildOfNode(actNode, "description");
const firstExpression: string | undefined = SchemaUtils.getChildOfNodePath(planDefNode, ["action", "condition", "expression", "expression"])?.value;
const conditionExpressions: string[] = firstExpression ? [firstExpression] : [];
return <div className="col-lg-3 col-md-4 col-sm-6" key={i}>
<Folder
actTitle={actTitleNode?.value ? actTitleNode.value : "Untitled AD"}
planTitle={planTitleNode?.value ? planTitleNode.value : "Untitled PD"}
conditionExpressions={conditionExpressions}
profile={SchemaUtils.toFhir(resource, false).meta?.profile?.[0] ?? ""}
wait={i*25}
index={i}
/>
</div>
})
}
return <div className="col-lg-3 col-md-4 col-sm-6" key={i*2}>
<Folder
actTitle={actTitleNode?.value ? actTitleNode.value : "Untitled AD"}
planTitle={planTitleNode?.value ? planTitleNode.value : "Untitled PD"}
actDesc={actDescNode?.value ? actDescNode.value : ""}
conditionExpressions={conditionExpressions}
profile={SchemaUtils.toFhir(actNode, false).meta?.profile?.[0] ?? ""}
wait={i * 25}
index={i*2}
/>
</div>
})
}
{resources.length == 0 ? <div style={{ margin: "50px", marginTop: "40px" }}> <i>No Cards</i> </div> : undefined}
</div>
</div>
);
Expand Down
7 changes: 4 additions & 3 deletions src/simplified/folder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import {BaseCard} from"./baseCard";
import { CSSTransition } from 'react-transition-group';
import State from "../state";
import { CloseButton } from "react-bootstrap";
import { ACTIVITY_DEFINITION, getBorderPropsForType, PLAN_DEFINITION, profileToFriendlyResourceListEntry, profileToFriendlyResourceSelf } from "./nameHelpers";
import {getBorderPropsForType, PLAN_DEFINITION, profileToFriendlyResourceListEntry, profileToFriendlyResourceSelf } from "./nameHelpers";

interface FolderProps {
actTitle: string,
actDesc: string,
planTitle: string,
conditionExpressions: string[],
index: number,
Expand Down Expand Up @@ -52,11 +53,11 @@ export const Folder = (props: FolderProps) => {
<BaseCard header={PLAN_DEFINITION} title={props.planTitle}
content={
<span>
{props.actTitle} {props.conditionExpressions.length > 0 ? `WHEN ${props.conditionExpressions[0]} IS TRUE` : ""}
{props.actDesc} {props.conditionExpressions.length > 0 ? `WHEN ${props.conditionExpressions[0]} IS TRUE` : ""}
</span>
}/>
</div>
{State.get().bundle.resources.length > 2 &&
{
<div className="delete">
<CloseButton
onClick={(e) => {
Expand Down
Loading