@evaro/consultation-sdk 0.0.0-stage → 0.2.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/dist/index.mjs +21053 -0
- package/dist/types/Consultation.d.ts +39 -0
- package/dist/types/api.d.ts +20 -0
- package/dist/types/draftQueue.d.ts +22 -0
- package/dist/types/events.d.ts +15 -0
- package/dist/types/flow/answer.d.ts +46 -0
- package/dist/types/flow/form.d.ts +67 -0
- package/dist/types/flow/path.d.ts +34 -0
- package/dist/types/flow/questionnaire.d.ts +133 -0
- package/dist/types/flow/useConsultationFlow.d.ts +36 -0
- package/dist/types/flow/validations.d.ts +6 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/sanitize.d.ts +2 -0
- package/dist/types/targets/diagnosisRequest.d.ts +3 -0
- package/dist/types/targets/index.d.ts +3 -0
- package/dist/types/types.d.ts +71 -0
- package/dist/types/ui/ErrorNotice.d.ts +4 -0
- package/dist/types/ui/FormFlow.d.ts +9 -0
- package/dist/types/ui/Knockout.d.ts +8 -0
- package/dist/types/ui/Question.d.ts +21 -0
- package/dist/types/ui/ReadOnlyAnswers.d.ts +9 -0
- package/dist/types/ui/ShadowRoot.d.ts +6 -0
- package/dist/types/ui/form/Blocks.d.ts +6 -0
- package/dist/types/ui/form/FormField.d.ts +38 -0
- package/dist/types/ui/form/inputs.d.ts +28 -0
- package/dist/types/ui/primitives.d.ts +37 -0
- package/dist/types/useTargetSave.d.ts +30 -0
- package/package.json +36 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type VendorTheme } from './api';
|
|
2
|
+
import type { ConsultationEvent, ConsultationType, Environment, GetToken, SavedAnswers, Target } from './types';
|
|
3
|
+
export interface ConsultationProps {
|
|
4
|
+
/** The hash of the consultation to show. */
|
|
5
|
+
consultation: string;
|
|
6
|
+
/** The vendor hash. It selects the theme and goes to the API, as in Connect. */
|
|
7
|
+
vendor: string;
|
|
8
|
+
environment: Environment;
|
|
9
|
+
/** Where the answers are saved. With no target the package runs as a preview and saves nothing. */
|
|
10
|
+
target?: Target;
|
|
11
|
+
/** Returns the signed-in user's token for the target's calls. */
|
|
12
|
+
getToken?: GetToken;
|
|
13
|
+
/** Sends the target's calls to this NodeAPI URL in place of the environment's host, for example a local stub server. */
|
|
14
|
+
nodeApiUrl?: string;
|
|
15
|
+
/** Shows the target's saved answers. They cannot be changed or submitted. */
|
|
16
|
+
readOnly?: boolean;
|
|
17
|
+
/** Loads the unpublished version of the consultation. */
|
|
18
|
+
preview?: boolean;
|
|
19
|
+
/** `steps` shows one question per screen. `form` shows every question on one page, grouped by section. */
|
|
20
|
+
layout?: ConsultationLayout;
|
|
21
|
+
/** The label of the form layout's submit button. Defaults to "Submit". */
|
|
22
|
+
submitLabel?: string;
|
|
23
|
+
/** Reported as `consultation_type` on the events. Defaults to the consultation's own type. */
|
|
24
|
+
consultationType?: ConsultationType;
|
|
25
|
+
/** Called when the flow ends: after submit, or when the user closes the knockout screen. */
|
|
26
|
+
onDone?: () => void;
|
|
27
|
+
/** Receives the analytics signals, with the names and fields Connect uses. */
|
|
28
|
+
onEvent?: (event: ConsultationEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
export type ConsultationLayout = 'steps' | 'form';
|
|
31
|
+
export { DRAFT_DELAY_MS } from './useTargetSave';
|
|
32
|
+
export type Loaded = {
|
|
33
|
+
key: string;
|
|
34
|
+
attempt: number;
|
|
35
|
+
consultation: iConsultation;
|
|
36
|
+
theme: VendorTheme | null;
|
|
37
|
+
saved: SavedAnswers;
|
|
38
|
+
};
|
|
39
|
+
export declare function Consultation(props: ConsultationProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Environment } from './types';
|
|
2
|
+
export declare const apiHost: (environment: Environment) => string;
|
|
3
|
+
export declare const nodeApiHost: (environment: Environment) => string;
|
|
4
|
+
/** The NodeAPI base for target calls: `nodeApiUrl` when the host sets one, otherwise the environment's host. */
|
|
5
|
+
export declare function nodeApiBase(environment: Environment, nodeApiUrl?: string): string;
|
|
6
|
+
export declare class RequestError extends Error {
|
|
7
|
+
readonly status: number;
|
|
8
|
+
constructor(message: string, status: number);
|
|
9
|
+
}
|
|
10
|
+
/** The server refused a write because the answers changed in another place. */
|
|
11
|
+
export declare const isConflict: (error: unknown) => boolean;
|
|
12
|
+
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
13
|
+
export declare function requestJson<T>(url: string, init?: RequestInit): Promise<T>;
|
|
14
|
+
export interface VendorTheme {
|
|
15
|
+
colours?: Partial<Record<'100' | '300' | '500' | '700' | '900', string>>;
|
|
16
|
+
}
|
|
17
|
+
/** The consultation as Connect loads it. `preview` reads the unpublished version, which needs no token. */
|
|
18
|
+
export declare function fetchVendorConsultation(environment: Environment, hash: string, vendor: string, preview: boolean): Promise<iConsultation>;
|
|
19
|
+
/** The vendor's colours, loaded once for each vendor. A missing theme leaves Connect's default palette. */
|
|
20
|
+
export declare function fetchVendorTheme(environment: Environment, vendor: string): Promise<VendorTheme | null>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs one target's drafts and submits one after another, so an older write never lands after a newer one, and
|
|
3
|
+
* a reload can wait for the writes that are still open.
|
|
4
|
+
*/
|
|
5
|
+
export declare class DraftQueue {
|
|
6
|
+
private readonly onIdle;
|
|
7
|
+
private tail;
|
|
8
|
+
private open;
|
|
9
|
+
constructor(onIdle?: () => void);
|
|
10
|
+
/** Runs the task after the open ones. A failed task does not stop the queue. */
|
|
11
|
+
run<T>(task: () => Promise<T>): Promise<T>;
|
|
12
|
+
/** Settles when every task queued so far has settled. */
|
|
13
|
+
idle(): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** Runs a write on the target's queue. Only a write creates a queue, and the queue goes once it is idle. */
|
|
16
|
+
export declare function runOnDraftQueue<T>(key: string, task: () => Promise<T>): Promise<T>;
|
|
17
|
+
/** Settles when the target's open writes have settled. A target with no open writes settles at once. */
|
|
18
|
+
export declare function draftQueueIdle(key: string): Promise<unknown>;
|
|
19
|
+
/** How many targets have open writes. For tests. */
|
|
20
|
+
export declare const openDraftQueues: () => number;
|
|
21
|
+
/** Forgets every queue, so one test's open writes cannot hold up the next test. For tests. */
|
|
22
|
+
export declare const resetDraftQueues: () => void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConsultationEvent, ConsultationType } from './types';
|
|
2
|
+
export interface EventContext {
|
|
3
|
+
consultationHash: string;
|
|
4
|
+
experimentOf: string | undefined;
|
|
5
|
+
consultationType: ConsultationType;
|
|
6
|
+
}
|
|
7
|
+
/** Builds the signals with the same names and fields as Connect's `utils/analytics.ts`. */
|
|
8
|
+
export declare const consultationEvents: (context: EventContext) => {
|
|
9
|
+
started: () => ConsultationEvent;
|
|
10
|
+
completed: () => ConsultationEvent;
|
|
11
|
+
exited: () => ConsultationEvent;
|
|
12
|
+
viewed: (number: number, all: number) => ConsultationEvent;
|
|
13
|
+
questionViewed: (hash: string, skipped?: boolean) => ConsultationEvent;
|
|
14
|
+
questionAnswered: (hash: string, skipped?: boolean) => ConsultationEvent;
|
|
15
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type AnswerValue = string | string[];
|
|
2
|
+
export type AnswerMap = Record<string, AnswerValue>;
|
|
3
|
+
type OptionLogic = iQuestion['options'][number]['logic'];
|
|
4
|
+
/** Types that wait for Continue: free text, multi-select and types with their own input. */
|
|
5
|
+
export declare const NON_AUTO_PROGRESS_TYPES: string[];
|
|
6
|
+
export interface AnswerState {
|
|
7
|
+
answers: AnswerMap;
|
|
8
|
+
optionHashes: AnswerMap;
|
|
9
|
+
}
|
|
10
|
+
export type Navigation = {
|
|
11
|
+
kind: 'none';
|
|
12
|
+
} | {
|
|
13
|
+
kind: 'next';
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'goto';
|
|
16
|
+
hash: string;
|
|
17
|
+
};
|
|
18
|
+
export interface AnswerResult extends AnswerState {
|
|
19
|
+
/** The jump to hold until Continue. `undefined` leaves the current one. */
|
|
20
|
+
nextOverride: string | null | undefined;
|
|
21
|
+
navigation: Navigation;
|
|
22
|
+
}
|
|
23
|
+
interface AnswerInput {
|
|
24
|
+
question: iQuestion;
|
|
25
|
+
state: AnswerState;
|
|
26
|
+
value: AnswerValue;
|
|
27
|
+
logic?: OptionLogic;
|
|
28
|
+
/** False for a follow-up that is not the last of its group, or while one above it is blank. */
|
|
29
|
+
ownsProgression: boolean;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Where a checkbox answer jumps: the first ticked option that has a go-to. The shared next-question rule takes
|
|
33
|
+
* the first ticked option and then reads its go-to, which misses a jump on a later option.
|
|
34
|
+
*/
|
|
35
|
+
export declare function checkboxJump(question: iQuestion | undefined, answer: AnswerValue | null | undefined, optionHash?: AnswerValue | null): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* The jump the live flow holds for Continue on a question: one set in an open follow-up, then a checkbox jump
|
|
38
|
+
* on the main question. A main radio's own go-to is left to the shared next-question rule.
|
|
39
|
+
*
|
|
40
|
+
* The live flow keeps the jump of the last click, and click order is not saved, so a later follow-up in the
|
|
41
|
+
* group wins here. The two differ only when two open follow-ups both hold a jump, or one clears another's.
|
|
42
|
+
*/
|
|
43
|
+
export declare function heldJump(question: iQuestion, allQuestions: iQuestion[], answers: AnswerMap, optionHashes?: AnswerMap): string | null;
|
|
44
|
+
/** Applies one answer. Returns null when the click changes nothing, such as the same radio option again. */
|
|
45
|
+
export declare function applyAnswer({ question, state, value, logic, ownsProgression }: AnswerInput): AnswerResult | null;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Rules of the one-page form layout: which questions show, how they group, and what a submit checks and sends. */
|
|
2
|
+
import type { AnswerMap } from './answer';
|
|
3
|
+
import { type Walk } from './path';
|
|
4
|
+
type Section = NonNullable<iConsultation['sections']>[number];
|
|
5
|
+
export interface FormTable {
|
|
6
|
+
/** Headings, the first one for the row labels. */
|
|
7
|
+
columns: string[];
|
|
8
|
+
/** One list of question hashes for each row, in column order. The first question's title is the row label. */
|
|
9
|
+
rows: string[][];
|
|
10
|
+
}
|
|
11
|
+
export interface FormSection {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
/** Set when the section draws as a table; otherwise its questions show as a list. */
|
|
16
|
+
table: FormTable | null;
|
|
17
|
+
/** The section's main questions, in flow order. */
|
|
18
|
+
questions: string[];
|
|
19
|
+
}
|
|
20
|
+
/** What the page shows, in flow order. */
|
|
21
|
+
export type FormBlock = {
|
|
22
|
+
kind: 'questions';
|
|
23
|
+
questions: string[];
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'section';
|
|
26
|
+
section: FormSection;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'tabs';
|
|
29
|
+
name: string;
|
|
30
|
+
sections: FormSection[];
|
|
31
|
+
};
|
|
32
|
+
export interface FormExit {
|
|
33
|
+
questionHash: string;
|
|
34
|
+
title: string | null;
|
|
35
|
+
content: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface Form {
|
|
38
|
+
/** The main questions on the page, in flow order. */
|
|
39
|
+
path: string[];
|
|
40
|
+
/** Every question on the page: the main ones and their open follow-ups, in the order the page draws them. */
|
|
41
|
+
shown: string[];
|
|
42
|
+
blocks: FormBlock[];
|
|
43
|
+
/** Questions that may stay empty: a table section's questions that are not Yes / No, and one-tick checkboxes. */
|
|
44
|
+
optional: Set<string>;
|
|
45
|
+
/** An answer with an exit. The questions after it are hidden. */
|
|
46
|
+
exit: FormExit | null;
|
|
47
|
+
}
|
|
48
|
+
/** A radio question whose only options are Yes and No. The form draws it as a Yes / No pair. */
|
|
49
|
+
export declare function isYesNo(question: iQuestion | undefined): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* A table section reads its main questions row by row, in consultation order: one question for each heading
|
|
52
|
+
* after the first. A row a jump or an exit hides as a whole is left out. When the questions do not fill whole
|
|
53
|
+
* rows, or an answer hides part of a row, the section shows as a list, so a cell can never move under the wrong
|
|
54
|
+
* row label.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sectionTable(section: Section, questions: string[], shown: Set<string>): FormTable | null;
|
|
57
|
+
/**
|
|
58
|
+
* What the page shows: the flow from the first question. A section's questions show together, where its first
|
|
59
|
+
* question comes in the flow, and questions with no section show where they come. Sections next to each other
|
|
60
|
+
* with the same group show as tabs. Follow-ups show under their parent, so only main questions are grouped.
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildForm(walk: Walk, consultation: Pick<iConsultation, 'hash' | 'sections'>): Form;
|
|
63
|
+
/** Questions on the page that still need an answer, in page order. Optional questions are never missing. */
|
|
64
|
+
export declare function missingAnswers(form: Form, walk: Walk): string[];
|
|
65
|
+
/** The answers on the page: what a draft saves and a submit sends. Answers of a branch a jump leaves are dropped. */
|
|
66
|
+
export declare function formAnswers(form: Form, walk: Walk): AnswerMap;
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type AnswerMap } from './answer';
|
|
2
|
+
/** A question and its open follow-ups, walking nested ones. */
|
|
3
|
+
export declare function withFollowUps(hash: string, allQuestions: iQuestion[], answers: AnswerMap, optionHashes: AnswerMap, into: Set<string>): void;
|
|
4
|
+
/**
|
|
5
|
+
* Only the answers on the path the user took. Going Back and choosing another option leaves the answers of the
|
|
6
|
+
* old branch in state; they must not be saved as if they were given on this path.
|
|
7
|
+
*/
|
|
8
|
+
export declare function answersOnPath(history: string[], allQuestions: iQuestion[], answers: AnswerMap, optionHashes: AnswerMap): AnswerMap;
|
|
9
|
+
/** The consultation and the answers that a walk through the flow follows. */
|
|
10
|
+
export interface Walk {
|
|
11
|
+
allQuestions: iQuestion[];
|
|
12
|
+
visibleQuestions: iQuestion[];
|
|
13
|
+
consultationHash: string;
|
|
14
|
+
answers: AnswerMap;
|
|
15
|
+
optionHashes: AnswerMap;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Where a saved draft opens: walks the flow from the first question while each one holds a complete answer.
|
|
19
|
+
* Returns the questions passed on the way, with the one to show last.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resumeHistory(walk: Walk): string[];
|
|
22
|
+
/**
|
|
23
|
+
* The one-page form's main questions: the flow from the first question, where only an answer takes a jump, so an
|
|
24
|
+
* unanswered question goes on to its default next one. It stops at an answer with an exit.
|
|
25
|
+
*/
|
|
26
|
+
export declare function formPath(walk: Walk): string[];
|
|
27
|
+
/**
|
|
28
|
+
* The answers a draft keeps: the path behind the user, and ahead of them the questions the current answers
|
|
29
|
+
* still lead to. After Back, answers further on survive while the user edits, even when an edit leaves the
|
|
30
|
+
* current question unfinished. Answers of a branch the user left are dropped.
|
|
31
|
+
*/
|
|
32
|
+
export declare function draftAnswers(history: string[], walk: Walk): AnswerMap;
|
|
33
|
+
/** Whether every question on the path has a complete answer, so the answers can be submitted. */
|
|
34
|
+
export declare function isPathComplete(history: string[], allQuestions: iQuestion[], answers: AnswerMap, optionHashes: AnswerMap): boolean;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic functions for the questionnaire consultation flow.
|
|
3
|
+
* Used by QuestionnaireManager and Question so the behaviour lives in one place.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Returns only the questions that should be visible to the patient:
|
|
7
|
+
* - excludes follow-up questions (shown inline)
|
|
8
|
+
* - excludes questions whose hash is in the hidden-answers map
|
|
9
|
+
* - excludes questions that don't apply to the patient's age or gender
|
|
10
|
+
*/
|
|
11
|
+
export declare function filterVisibleQuestions(questions: iQuestion[], hiddenAnswers: Record<string, unknown>, demographics?: PatientDemographics | null): iQuestion[];
|
|
12
|
+
/** Age and gender known before the consultation starts, when the patient is picked or described up front. */
|
|
13
|
+
export interface PatientDemographics {
|
|
14
|
+
age: number | null;
|
|
15
|
+
gender: 'male' | 'female' | null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether a question doesn't apply to the patient. Questions carry no age or gender conditions yet, so
|
|
19
|
+
* nothing is hidden; the rules go here once the API sends them.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isHiddenForPatient(_question: iQuestion, _demographics: PatientDemographics | null): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Clears every jump naming a question the list no longer holds, so the flow falls through in order rather
|
|
24
|
+
* than opening one that cannot render. Follow-ups too: a pending one that renders nothing holds Continue shut.
|
|
25
|
+
*/
|
|
26
|
+
export declare function withReachableJumps(questions: iQuestion[]): iQuestion[];
|
|
27
|
+
/**
|
|
28
|
+
* A `dosage` question is an unanswerable template until the API expands it under the same hash, as a radio.
|
|
29
|
+
* One that never expanded is dropped — nothing renders the type — and jumps aimed at it are cleared with it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withExpandedDosage(questions: iQuestion[], expanded?: iQuestion | null): iQuestion[];
|
|
32
|
+
/**
|
|
33
|
+
* Whether the flow can open this question — for the `q` in the URL, the one hash nothing upstream vets.
|
|
34
|
+
* Plain membership, so hand it the visible set: a follow-up is asked inline and a carried answer is hidden,
|
|
35
|
+
* and neither has a screen of its own.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isRenderableQuestion(hash: string | null | undefined, questions: iQuestion[]): hash is string;
|
|
38
|
+
/**
|
|
39
|
+
* The BMI range a reading falls in. Ranges may share a bound (24-27 then 27-28), and the later one
|
|
40
|
+
* takes the shared value, so the patient's feedback and their routing agree on which range applies.
|
|
41
|
+
*/
|
|
42
|
+
export declare function findBmiRange(options: iQuestion['options'] | undefined, bmi: number): iQuestion['options'][number] | undefined;
|
|
43
|
+
/** The BMI a stored answer carries (`height=…cm weight=…kg bmi=…`), or null when it has none. */
|
|
44
|
+
export declare function bmiFromAnswer(answer: string | string[] | null | undefined): number | null;
|
|
45
|
+
/**
|
|
46
|
+
* Determines which question to navigate to next. Throws when questionIndex is negative.
|
|
47
|
+
* Priority order:
|
|
48
|
+
* 1. nextQuestionHashOverride (set by inline follow-up logic)
|
|
49
|
+
* 2. the go-to on the answered option, or on the range a BMI answer falls in
|
|
50
|
+
* 3. consultation-level logic.next on the current question
|
|
51
|
+
* 4. the standard next question in sequence
|
|
52
|
+
*
|
|
53
|
+
* The final branch reads `.hash` off the next question directly (no optional
|
|
54
|
+
* chaining) so an out-of-bounds index throws here, at the source, rather than
|
|
55
|
+
* silently returning undefined and crashing a component further down.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolveNextQuestionHash(questions: iQuestion[], questionIndex: number, nextQuestionHashOverride: string | null, currentQuestion: iQuestion | undefined, consultationHash: string, answer?: string | string[] | null): string;
|
|
58
|
+
/**
|
|
59
|
+
* Where a navigation target lands once hidden questions are taken into account:
|
|
60
|
+
* the target itself when visible, else the first visible question after both the
|
|
61
|
+
* target and the current question in consultation order, else null. A follow-up
|
|
62
|
+
* or unknown target continues after the current question.
|
|
63
|
+
*/
|
|
64
|
+
export declare function resolveVisibleTarget(allQuestions: iQuestion[], visibleQuestions: iQuestion[], targetHash: string, currentHash?: string | null): string | null;
|
|
65
|
+
type Option = iQuestion['options'][0];
|
|
66
|
+
type OptionLogic = Option['logic'];
|
|
67
|
+
/**
|
|
68
|
+
* Whether an option is the current answer — matched by option hash (primary)
|
|
69
|
+
* or by label (backward-compatibility fallback).
|
|
70
|
+
*/
|
|
71
|
+
export declare function isOptionSelected(option: Option, answer: string | string[] | null | undefined, answerOptionHash: string | string[] | null | undefined): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the logic entries for whichever options match the current answer.
|
|
74
|
+
* Handles both single-value (radio) and multi-value (checkbox) answers.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getMatchedOptionLogics(answer: string | string[] | null | undefined, answerOptionHash: string | string[] | null | undefined, options: iQuestion['options']): OptionLogic[];
|
|
77
|
+
/**
|
|
78
|
+
* Collects all inline follow-up question hashes from the active logics,
|
|
79
|
+
* de-duplicating so each follow-up appears once.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getFollowUpHashes(logics: OptionLogic[]): string[];
|
|
82
|
+
/** Whether an option logic carries an exit condition. */
|
|
83
|
+
export declare function hasExitLogic(logic: OptionLogic | undefined): boolean;
|
|
84
|
+
export declare const FREE_TEXT_TYPES: string[];
|
|
85
|
+
/** Free text needs enough to be worth reading, so a stray character does not unlock Continue. */
|
|
86
|
+
export declare function isFreeTextTooShort(type: string, answer: string | string[] | null | undefined): boolean;
|
|
87
|
+
type AnswerMap = Record<string, string | string[]>;
|
|
88
|
+
/** Whether every follow-up in the group is answered, walking nested ones. `seen` guards a chain that loops back. */
|
|
89
|
+
export declare function areFollowUpsAnswered(followUpHashes: string[], questions: iQuestion[], answers: AnswerMap, optionHashes: AnswerMap, seen?: Set<string>): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Every top-level question, plus the follow-ups the selected options open, walking nested ones.
|
|
92
|
+
* Jumps (`logic.next`) are not followed, so a top-level question the run skipped still counts as reached.
|
|
93
|
+
*/
|
|
94
|
+
export declare function reachableQuestionHashes(questions: iQuestion[], answers: AnswerMap, optionHashes: AnswerMap): Set<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Decides whether the Continue button should be rendered:
|
|
97
|
+
* - there is an answer
|
|
98
|
+
* - no exit condition is triggered
|
|
99
|
+
* - no inline follow-up questions are pending
|
|
100
|
+
* - the question type doesn't auto-navigate (radio) or has its own submit (uploader)
|
|
101
|
+
* - date fields must be fully filled (length === 10 for DD-MM-YYYY)
|
|
102
|
+
* - the answer passes validation
|
|
103
|
+
*/
|
|
104
|
+
export declare function shouldShowButton(type: string, answer: string | string[] | null | undefined, exitQuestion: OptionLogic | undefined, followUpHashes: string[], hasValidationError: boolean | null | undefined): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Computes the new checkbox answer when a value is toggled.
|
|
107
|
+
* - If the value is already in the array it is removed.
|
|
108
|
+
* - If it is not, it is appended.
|
|
109
|
+
* - Returns null when the resulting array is empty (clears the answer).
|
|
110
|
+
*/
|
|
111
|
+
export declare function applyCheckboxToggle(currentAnswer: string | string[] | null | undefined, value: string): string[] | null;
|
|
112
|
+
/**
|
|
113
|
+
* The microcopy shown above the answer options, telling the patient how the answer
|
|
114
|
+
* behaves. Set by the answer type rather than authored per question, so every
|
|
115
|
+
* consultation reads the same way. Types not listed have no hint.
|
|
116
|
+
*/
|
|
117
|
+
export declare function inputHintFor(type: string): string | null;
|
|
118
|
+
/**
|
|
119
|
+
* CMS HTML with its non-breaking spaces made ordinary. Copy pasted into the editor can arrive
|
|
120
|
+
* with every space non-breaking, which leaves a line nowhere to wrap but mid-word.
|
|
121
|
+
*/
|
|
122
|
+
export declare function withBreakableSpaces(html: string | null | undefined): string;
|
|
123
|
+
/**
|
|
124
|
+
* The flags an answer raises, derived from the answer itself: those on the options the
|
|
125
|
+
* patient has picked, plus the `flags_unselected` of the options they left alone. A
|
|
126
|
+
* clinical flag sometimes turns on an absent answer, and an option can only flag itself
|
|
127
|
+
* when chosen. Deriving both sides from the selection means the stored flags always match
|
|
128
|
+
* the stored answer, rather than only the option that was clicked last.
|
|
129
|
+
*/
|
|
130
|
+
export declare function collectFlags(selectedLabels: string[], options: iQuestion['options'] | undefined): string[] | null;
|
|
131
|
+
/** Where "Review answer" returns to. Same-origin paths only, since the value arrives in the URL; null leaves the caller on its own path. */
|
|
132
|
+
export declare function resolveExitReturnUrl(returnTo: string | undefined | null, mainQuestionHash: string | undefined | null): string | null;
|
|
133
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ConsultationEvent, ConsultationType } from '../types';
|
|
2
|
+
import { type AnswerMap, type AnswerValue } from './answer';
|
|
3
|
+
type OptionLogic = iQuestion['options'][number]['logic'];
|
|
4
|
+
export interface ExitState {
|
|
5
|
+
questionHash: string;
|
|
6
|
+
title: string | null;
|
|
7
|
+
content: string | null;
|
|
8
|
+
}
|
|
9
|
+
export type Phase = 'question' | 'exit' | 'complete';
|
|
10
|
+
interface Options {
|
|
11
|
+
consultation: iConsultation;
|
|
12
|
+
consultationType: ConsultationType;
|
|
13
|
+
initialAnswers: AnswerMap;
|
|
14
|
+
/** False when a saved draft is reopened, so the start is not counted twice. */
|
|
15
|
+
isNewStart: boolean;
|
|
16
|
+
emit: (event: ConsultationEvent) => void;
|
|
17
|
+
}
|
|
18
|
+
export declare function useConsultationFlow({ consultation, consultationType, initialAnswers, isNewStart, emit }: Options): {
|
|
19
|
+
allQuestions: iQuestion[];
|
|
20
|
+
questions: iQuestion[];
|
|
21
|
+
current: string;
|
|
22
|
+
questionIndex: number;
|
|
23
|
+
history: string[];
|
|
24
|
+
answers: AnswerMap;
|
|
25
|
+
optionHashes: AnswerMap;
|
|
26
|
+
phase: Phase;
|
|
27
|
+
exit: ExitState;
|
|
28
|
+
revision: number;
|
|
29
|
+
canGoBack: boolean;
|
|
30
|
+
answer: (question: iQuestion, value: AnswerValue, logic: OptionLogic | undefined, ownsProgression: boolean) => void;
|
|
31
|
+
nextQuestion: () => void;
|
|
32
|
+
back: () => void;
|
|
33
|
+
reviewExitAnswer: () => void;
|
|
34
|
+
restart: () => void;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Environment, GetToken, TargetAdapter } from '../types';
|
|
2
|
+
/** Assessment answers stored on a diagnosis request, through NodeAPI with the Prescriber App user's token. */
|
|
3
|
+
export declare function diagnosisRequestTarget(environment: Environment, id: string, getToken: GetToken, nodeApiUrl?: string): TargetAdapter;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Environment, GetToken, Target, TargetAdapter } from '../types';
|
|
2
|
+
/** One adapter for each target type. To save answers in a new place, add a type here, not code in the host. */
|
|
3
|
+
export declare function createTargetAdapter(target: Target, environment: Environment, getToken: GetToken, nodeApiUrl?: string): TargetAdapter;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export type Environment = 'development' | 'staging' | 'production';
|
|
2
|
+
/** An answer in Connect's format: option labels for choice types, text for the rest, `DD-MM-YYYY` for a date. */
|
|
3
|
+
export type Answer = string | string[];
|
|
4
|
+
export type Answers = Record<string, Answer>;
|
|
5
|
+
/** Where the answers are saved. With no target the package runs as a preview and saves nothing. */
|
|
6
|
+
export type Target = {
|
|
7
|
+
type: 'diagnosis-request';
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
export type GetToken = () => Promise<string | null> | string | null;
|
|
11
|
+
export type ConsultationType = 'treatment' | 'renew' | 'reorder' | 'review' | 'care' | (string & {});
|
|
12
|
+
/** Saved answers as a target returns them. */
|
|
13
|
+
export interface SavedAnswers {
|
|
14
|
+
answers: Answers;
|
|
15
|
+
status: 'empty' | 'draft' | 'submitted';
|
|
16
|
+
/** The consultation version the answers were given against, when the target knows it. */
|
|
17
|
+
consultationVersion: string | null;
|
|
18
|
+
submittedAt: string | null;
|
|
19
|
+
/** The server's version of the saved answers. A draft must start from it, or the server refuses the draft. */
|
|
20
|
+
version: number | null;
|
|
21
|
+
}
|
|
22
|
+
/** The calls the package makes for one target type. The adapter is the only code that knows the endpoints. */
|
|
23
|
+
export interface TargetAdapter {
|
|
24
|
+
/** The version of the last load or saved draft. Each draft sends it. */
|
|
25
|
+
readonly version: number | null;
|
|
26
|
+
load: () => Promise<SavedAnswers>;
|
|
27
|
+
saveDraft: (answers: Answers) => Promise<void>;
|
|
28
|
+
submit: (answers: Answers) => Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
interface ConsultationFields {
|
|
31
|
+
questionnaire_hash: string;
|
|
32
|
+
experiment_of: string | undefined;
|
|
33
|
+
consultation_type: ConsultationType;
|
|
34
|
+
}
|
|
35
|
+
interface QuestionFields {
|
|
36
|
+
question_hash: string;
|
|
37
|
+
consultation_hash: string;
|
|
38
|
+
is_skipped: boolean;
|
|
39
|
+
experiment_of: string | undefined;
|
|
40
|
+
consultation_type: ConsultationType;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* An analytics signal. The names and fields are the ones Connect sends, so a host can forward them as they are.
|
|
44
|
+
* The package does not send them anywhere itself.
|
|
45
|
+
*/
|
|
46
|
+
export type ConsultationEvent = {
|
|
47
|
+
name: 'consultation_started';
|
|
48
|
+
data: ConsultationFields;
|
|
49
|
+
} | {
|
|
50
|
+
name: 'consultation_viewed';
|
|
51
|
+
data: {
|
|
52
|
+
question_number: string;
|
|
53
|
+
question_number_of_total: string;
|
|
54
|
+
};
|
|
55
|
+
} | {
|
|
56
|
+
name: 'question_viewed';
|
|
57
|
+
data: QuestionFields;
|
|
58
|
+
} | {
|
|
59
|
+
name: 'question_answered';
|
|
60
|
+
data: QuestionFields;
|
|
61
|
+
} | {
|
|
62
|
+
name: 'consultation_completed';
|
|
63
|
+
data: ConsultationFields;
|
|
64
|
+
} | {
|
|
65
|
+
name: 'consultation_exited';
|
|
66
|
+
data: {
|
|
67
|
+
questionnaire_hash: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export type ConsultationEventName = ConsultationEvent['name'];
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ConsultationProps, Loaded } from '../Consultation';
|
|
2
|
+
import type { TargetAdapter } from '../types';
|
|
3
|
+
export declare const FormFlow: ({ loaded, adapter, consultationType, submitLabel, onDone, onEvent, queueKey, onConflict, onSaved }: ConsultationProps & {
|
|
4
|
+
loaded: Loaded;
|
|
5
|
+
adapter: TargetAdapter | null;
|
|
6
|
+
queueKey: string;
|
|
7
|
+
onConflict: () => void;
|
|
8
|
+
onSaved: () => void;
|
|
9
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnswerMap, AnswerValue } from '../flow/answer';
|
|
2
|
+
type OptionLogic = iQuestion['options'][number]['logic'];
|
|
3
|
+
/** The types this version can fill in. The rest show a notice. */
|
|
4
|
+
export declare const FILL_TYPES: string[];
|
|
5
|
+
interface Props {
|
|
6
|
+
hash: string;
|
|
7
|
+
consultationHash: string;
|
|
8
|
+
questions: iQuestion[];
|
|
9
|
+
answers: AnswerMap;
|
|
10
|
+
optionHashes: AnswerMap;
|
|
11
|
+
onAnswer: (question: iQuestion, value: AnswerValue, logic: OptionLogic | undefined, ownsProgression: boolean) => void;
|
|
12
|
+
onContinue: () => void;
|
|
13
|
+
isMainQuestion?: boolean;
|
|
14
|
+
isLastFollowUp?: boolean;
|
|
15
|
+
precedingFollowUpsAnswered?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
/** In a preview a type this version cannot fill in can be skipped, so the rest of the flow stays reachable. */
|
|
18
|
+
allowSkipUnsupported?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const Question: ({ hash, consultationHash, questions, answers, optionHashes, onAnswer, onContinue, isMainQuestion, isLastFollowUp, precedingFollowUpsAnswered, disabled, allowSkipUnsupported }: Props) => import("react").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Saved answers, one question after another. Every type shows its answer, including the ones that cannot be filled in here. */
|
|
2
|
+
import type { AnswerMap } from '../flow/answer';
|
|
3
|
+
interface Props {
|
|
4
|
+
questions: iQuestion[];
|
|
5
|
+
answers: AnswerMap;
|
|
6
|
+
submittedAt: string | null;
|
|
7
|
+
}
|
|
8
|
+
export declare const ReadOnlyAnswers: ({ questions, answers, submittedAt }: Props) => import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
/** Renders the children in a shadow root with the package CSS, so the host CSS and the package CSS do not touch. */
|
|
3
|
+
export declare const ShadowRoot: ({ children, style }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
style?: CSSProperties;
|
|
6
|
+
}) => import("react").JSX.Element;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AnswerMap } from '../../flow/answer';
|
|
2
|
+
import { type Answer } from './inputs';
|
|
3
|
+
export interface FieldContext {
|
|
4
|
+
consultationHash: string;
|
|
5
|
+
questions: iQuestion[];
|
|
6
|
+
answers: AnswerMap;
|
|
7
|
+
optionHashes: AnswerMap;
|
|
8
|
+
onAnswer: Answer;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Questions to mark as needing an answer, after a submit that failed the check. */
|
|
11
|
+
missing: Set<string>;
|
|
12
|
+
/** The question a failed submit points at. A new tick asks again, so tabs open the one that holds it. */
|
|
13
|
+
focus: {
|
|
14
|
+
hash: string;
|
|
15
|
+
tick: number;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
export declare const MissingNote: ({ show }: {
|
|
19
|
+
show: boolean;
|
|
20
|
+
}) => import("react").JSX.Element;
|
|
21
|
+
export declare const getOpenFollowUps: (hash: string, context: FieldContext) => string[];
|
|
22
|
+
/** The open follow-ups of a question, indented under it. */
|
|
23
|
+
export declare const FollowUps: ({ hash, context }: {
|
|
24
|
+
hash: string;
|
|
25
|
+
context: FieldContext;
|
|
26
|
+
}) => import("react").JSX.Element;
|
|
27
|
+
/** `spaced` adds the gap a field needs inside a section, where Yes / No rows sit with no gap. */
|
|
28
|
+
export declare const FormField: ({ hash, context, spaced }: {
|
|
29
|
+
hash: string;
|
|
30
|
+
context: FieldContext;
|
|
31
|
+
spaced?: boolean;
|
|
32
|
+
}) => import("react").JSX.Element;
|
|
33
|
+
/** A question as a table cell, with no label of its own. */
|
|
34
|
+
export declare const Cell: ({ hash, column, context }: {
|
|
35
|
+
hash: string;
|
|
36
|
+
column: string;
|
|
37
|
+
context: FieldContext;
|
|
38
|
+
}) => import("react").JSX.Element;
|