@evaro/consultation-sdk 0.1.0 → 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 +2 -2
- package/dist/index.mjs +1309 -152
- package/dist/types/Consultation.d.ts +17 -2
- package/dist/types/api.d.ts +5 -0
- package/dist/types/draftQueue.d.ts +22 -0
- package/dist/types/flow/form.d.ts +67 -0
- package/dist/types/flow/path.d.ts +7 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/targets/diagnosisRequest.d.ts +1 -1
- package/dist/types/targets/index.d.ts +1 -1
- package/dist/types/types.d.ts +4 -0
- package/dist/types/ui/ErrorNotice.d.ts +4 -0
- package/dist/types/ui/FormFlow.d.ts +9 -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/useTargetSave.d.ts +30 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type VendorTheme } from './api';
|
|
2
|
+
import type { ConsultationEvent, ConsultationType, Environment, GetToken, SavedAnswers, Target } from './types';
|
|
2
3
|
export interface ConsultationProps {
|
|
3
4
|
/** The hash of the consultation to show. */
|
|
4
5
|
consultation: string;
|
|
@@ -9,10 +10,16 @@ export interface ConsultationProps {
|
|
|
9
10
|
target?: Target;
|
|
10
11
|
/** Returns the signed-in user's token for the target's calls. */
|
|
11
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;
|
|
12
15
|
/** Shows the target's saved answers. They cannot be changed or submitted. */
|
|
13
16
|
readOnly?: boolean;
|
|
14
17
|
/** Loads the unpublished version of the consultation. */
|
|
15
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;
|
|
16
23
|
/** Reported as `consultation_type` on the events. Defaults to the consultation's own type. */
|
|
17
24
|
consultationType?: ConsultationType;
|
|
18
25
|
/** Called when the flow ends: after submit, or when the user closes the knockout screen. */
|
|
@@ -20,5 +27,13 @@ export interface ConsultationProps {
|
|
|
20
27
|
/** Receives the analytics signals, with the names and fields Connect uses. */
|
|
21
28
|
onEvent?: (event: ConsultationEvent) => void;
|
|
22
29
|
}
|
|
23
|
-
export
|
|
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
|
+
};
|
|
24
39
|
export declare function Consultation(props: ConsultationProps): import("react").JSX.Element;
|
package/dist/types/api.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { Environment } from './types';
|
|
2
2
|
export declare const apiHost: (environment: Environment) => string;
|
|
3
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;
|
|
4
6
|
export declare class RequestError extends Error {
|
|
5
7
|
readonly status: number;
|
|
6
8
|
constructor(message: string, status: number);
|
|
7
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;
|
|
8
13
|
export declare function requestJson<T>(url: string, init?: RequestInit): Promise<T>;
|
|
9
14
|
export interface VendorTheme {
|
|
10
15
|
colours?: Partial<Record<'100' | '300' | '500' | '700' | '900', string>>;
|
|
@@ -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,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 {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
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;
|
|
2
4
|
/**
|
|
3
5
|
* Only the answers on the path the user took. Going Back and choosing another option leaves the answers of the
|
|
4
6
|
* old branch in state; they must not be saved as if they were given on this path.
|
|
@@ -17,6 +19,11 @@ export interface Walk {
|
|
|
17
19
|
* Returns the questions passed on the way, with the one to show last.
|
|
18
20
|
*/
|
|
19
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[];
|
|
20
27
|
/**
|
|
21
28
|
* The answers a draft keeps: the path behind the user, and ahead of them the questions the current answers
|
|
22
29
|
* still lead to. After Back, answers further on survive while the user edits, even when an edit leaves the
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Consultation, type ConsultationProps } from './Consultation';
|
|
1
|
+
export { Consultation, type ConsultationLayout, type ConsultationProps } from './Consultation';
|
|
2
2
|
export type { Answer, Answers, ConsultationEvent, ConsultationEventName, ConsultationType, Environment, GetToken, SavedAnswers, Target, TargetAdapter } from './types';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Environment, GetToken, TargetAdapter } from '../types';
|
|
2
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): TargetAdapter;
|
|
3
|
+
export declare function diagnosisRequestTarget(environment: Environment, id: string, getToken: GetToken, nodeApiUrl?: string): TargetAdapter;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { Environment, GetToken, Target, TargetAdapter } from '../types';
|
|
2
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): TargetAdapter;
|
|
3
|
+
export declare function createTargetAdapter(target: Target, environment: Environment, getToken: GetToken, nodeApiUrl?: string): TargetAdapter;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -16,9 +16,13 @@ export interface SavedAnswers {
|
|
|
16
16
|
/** The consultation version the answers were given against, when the target knows it. */
|
|
17
17
|
consultationVersion: string | null;
|
|
18
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;
|
|
19
21
|
}
|
|
20
22
|
/** The calls the package makes for one target type. The adapter is the only code that knows the endpoints. */
|
|
21
23
|
export interface TargetAdapter {
|
|
24
|
+
/** The version of the last load or saved draft. Each draft sends it. */
|
|
25
|
+
readonly version: number | null;
|
|
22
26
|
load: () => Promise<SavedAnswers>;
|
|
23
27
|
saveDraft: (answers: Answers) => Promise<void>;
|
|
24
28
|
submit: (answers: Answers) => Promise<void>;
|
|
@@ -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,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;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AnswerMap, AnswerValue } from '../../flow/answer';
|
|
2
|
+
type OptionLogic = iQuestion['options'][number]['logic'];
|
|
3
|
+
export type Answer = (question: iQuestion, value: AnswerValue, logic: OptionLogic | undefined) => void;
|
|
4
|
+
export interface InputProps {
|
|
5
|
+
question: iQuestion;
|
|
6
|
+
answers: AnswerMap;
|
|
7
|
+
optionHashes: AnswerMap;
|
|
8
|
+
onAnswer: Answer;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Names the field for screen readers when no visible label sits next to it, as in a table cell. */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const fieldClass = "w-full rounded-2xl border border-gray-300 bg-white px-4 text-sm font-normal text-gray-900 placeholder:text-gray-400 focus:border-primary-700 focus:ring-primary-700 disabled:opacity-60";
|
|
14
|
+
export declare const plainText: (html: string | undefined) => string;
|
|
15
|
+
/** Options as a joined pair of buttons, such as Yes / No. They are radio inputs, so the keyboard works as usual. */
|
|
16
|
+
export declare const Segmented: ({ question, answers, optionHashes, onAnswer, disabled, label }: InputProps) => import("react").JSX.Element;
|
|
17
|
+
/** A radio question as a dropdown. The native list opens outside the shadow root's styles but works everywhere. */
|
|
18
|
+
export declare const Select: ({ question, answers, optionHashes, onAnswer, disabled, label }: InputProps) => import("react").JSX.Element;
|
|
19
|
+
/** Free text, a phone number or a date. `multiline` draws the tall box used for notes. */
|
|
20
|
+
export declare const TextInput: ({ question, answers, onAnswer, disabled, label, multiline, placeholder }: InputProps & {
|
|
21
|
+
multiline?: boolean;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
}) => import("react").JSX.Element;
|
|
24
|
+
/** Checkbox options as small ticks. A question with one option draws as one tick with its label. */
|
|
25
|
+
export declare const Checks: ({ question, answers, optionHashes, onAnswer, disabled }: InputProps) => import("react").JSX.Element;
|
|
26
|
+
/** Radio options that are not Yes / No, as a list of round buttons. */
|
|
27
|
+
export declare const Radios: ({ question, answers, optionHashes, onAnswer, disabled }: InputProps) => import("react").JSX.Element;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Answers, TargetAdapter } from './types';
|
|
2
|
+
export declare const DRAFT_DELAY_MS = 600;
|
|
3
|
+
export type SaveState = 'idle' | 'saving' | 'saved' | 'error';
|
|
4
|
+
export type SubmitState = 'idle' | 'submitting' | 'submitted' | 'error';
|
|
5
|
+
interface Options {
|
|
6
|
+
adapter: TargetAdapter | null;
|
|
7
|
+
/** The target's queue key. Its drafts and submits run one after another, and a reload of it waits for them. */
|
|
8
|
+
queueKey: string;
|
|
9
|
+
/** Bumped on every change the user makes. */
|
|
10
|
+
revision: number;
|
|
11
|
+
/** The answers a draft sends now. */
|
|
12
|
+
latestDraft: () => Answers;
|
|
13
|
+
onDone?: () => void;
|
|
14
|
+
/** Called once a submit has landed, before `onDone`. */
|
|
15
|
+
onSubmitted?: () => void;
|
|
16
|
+
/** The server refused a write because the answers changed in another place. */
|
|
17
|
+
onConflict: () => void;
|
|
18
|
+
/** A draft or the submit was saved. */
|
|
19
|
+
onSaved: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const errorMessage: (error: unknown) => string;
|
|
22
|
+
export declare function useTargetSave({ adapter, queueKey, revision, latestDraft, onDone, onSubmitted, onConflict, onSaved }: Options): {
|
|
23
|
+
saveState: SaveState;
|
|
24
|
+
submitState: SubmitState;
|
|
25
|
+
submitError: string;
|
|
26
|
+
saveDraft: () => void;
|
|
27
|
+
submit: (answers: Answers) => void;
|
|
28
|
+
rejectSubmit: (message: string) => void;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
package/package.json
CHANGED