form-snapshots 1.0.6
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 +226 -0
- package/dist/index.d.mts +235 -0
- package/dist/index.d.ts +235 -0
- package/dist/index.js +1421 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1389 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
## form-snapshots
|
|
2
|
+
|
|
3
|
+
For a more complete, multi-page guide, see the **docs mini‑wiki** under `docs/`:
|
|
4
|
+
|
|
5
|
+
- [Documentation index](./docs/index.md)
|
|
6
|
+
|
|
7
|
+
**form-snapshots** is a small React library that adds **autosave + history snapshots** to your forms using **IndexedDB (Dexie)** under the hood.
|
|
8
|
+
|
|
9
|
+
It automatically stores form state while the user is typing, lets them resume unfinished forms, inspect past submissions, and debug failures, all without changing your backend.
|
|
10
|
+
|
|
11
|
+
### What this project is
|
|
12
|
+
|
|
13
|
+
- **React hooks and components** for:
|
|
14
|
+
- **Autosaving** form state to the browser on blur.
|
|
15
|
+
- **Restoring** the most recent snapshot back into the form.
|
|
16
|
+
- **Listing** previous sessions and their metadata (status code, error message, timestamps).
|
|
17
|
+
- **Devtools UI** to inspect and filter form sessions in development.
|
|
18
|
+
- **Storage layer** built on Dexie, so everything lives in the user’s browser (no server required).
|
|
19
|
+
|
|
20
|
+
### What it is for
|
|
21
|
+
|
|
22
|
+
- **Long or complex forms** where users might navigate away or lose their work.
|
|
23
|
+
- **Clinical / enterprise / back-office apps** where losing form data is painful and audit/history is important.
|
|
24
|
+
- **Debugging submission problems** (you can see exactly what the user had filled + status codes / errors).
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install form-snapshots
|
|
32
|
+
# peer deps
|
|
33
|
+
npm install react react-dom dexie dexie-react-hooks react-hook-form
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Requires **React 18+**.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Core concepts
|
|
41
|
+
|
|
42
|
+
- **Session**: one editing “lifetime” of a form (from first visit until successful submit).
|
|
43
|
+
- **Snapshot**: a JSON representation of the form values at a point in time.
|
|
44
|
+
- **Storage**: by default, snapshots are stored in IndexedDB via Dexie.
|
|
45
|
+
|
|
46
|
+
You identify each form by a **`formName`** string; all hooks and tools use this key to group sessions.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Basic usage: `useFormSnapshots`
|
|
51
|
+
|
|
52
|
+
`useFormSnapshots` adds autosave and restore to a standard DOM form.
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { useFormSnapshots } from "form-snapshots"
|
|
56
|
+
|
|
57
|
+
export function ContactForm() {
|
|
58
|
+
const {
|
|
59
|
+
formRef,
|
|
60
|
+
handleBlur,
|
|
61
|
+
wrapSubmit,
|
|
62
|
+
restoreLatest,
|
|
63
|
+
isSubmitted,
|
|
64
|
+
} = useFormSnapshots("contact-form")
|
|
65
|
+
|
|
66
|
+
const onSubmit = wrapSubmit(() => {
|
|
67
|
+
// Perform your usual submit logic (fetch / mutation / etc.)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<form ref={formRef} onBlur={handleBlur} onSubmit={onSubmit}>
|
|
72
|
+
<input name="fullName" placeholder="Full name" />
|
|
73
|
+
<input name="email" type="email" placeholder="Email" />
|
|
74
|
+
<textarea name="message" placeholder="Message" />
|
|
75
|
+
|
|
76
|
+
<button type="submit">Send</button>
|
|
77
|
+
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={restoreLatest}
|
|
81
|
+
disabled={isSubmitted}
|
|
82
|
+
>
|
|
83
|
+
Restore last draft
|
|
84
|
+
</button>
|
|
85
|
+
</form>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**How it works:**
|
|
91
|
+
|
|
92
|
+
- On blur, the hook reads values from the DOM and saves them to IndexedDB.
|
|
93
|
+
- On mount, it restores the latest snapshot (if any) back into the form.
|
|
94
|
+
- On submit, it closes the session and marks it as `submitted` so new edits start a fresh session.
|
|
95
|
+
|
|
96
|
+
### Options
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
useFormSnapshots("contact-form", {
|
|
100
|
+
snapshotsLimit: 24 * 60 * 60 * 1000, // ms to retain history (default: 24h)
|
|
101
|
+
excludeFields: ["password", "patient.ssn"], // paths to omit from snapshots
|
|
102
|
+
getValues: () => snapshot, // custom snapshot source (for controlled forms)
|
|
103
|
+
applyValues: (snapshot) => {}, // custom restore logic (for controlled forms)
|
|
104
|
+
discardOnSubmit: true, // delete session + clear form after submit
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use `getValues` / `applyValues` when your form state lives in React state or is nested/structured, instead of plain DOM inputs.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Global config: `FormSnapshotsProvider`
|
|
113
|
+
|
|
114
|
+
You can configure defaults once at the application root.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { FormSnapshotsProvider } from "form-snapshots"
|
|
118
|
+
|
|
119
|
+
function App() {
|
|
120
|
+
return (
|
|
121
|
+
<FormSnapshotsProvider
|
|
122
|
+
value={{
|
|
123
|
+
defaultSnapshotsLimit: 7 * 24 * 60 * 60 * 1000, // keep 7 days
|
|
124
|
+
excludeFields: ["password", "patient.address"],
|
|
125
|
+
discardOnSubmit: false, // global default, can be overridden per form
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{/* your routes/components */}
|
|
129
|
+
</FormSnapshotsProvider>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Per-form options passed to `useFormSnapshots` are merged with this global config.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Devtools: `FormSnapshotsDevtools`
|
|
139
|
+
|
|
140
|
+
`FormSnapshotsDevtools` renders a small toggle button and a side panel that shows:
|
|
141
|
+
|
|
142
|
+
- All form sessions stored locally.
|
|
143
|
+
- Filters by **form name**, **submitted only**, and **errors only**.
|
|
144
|
+
- Timestamps, relative age, status codes, and error messages.
|
|
145
|
+
- Inline table view of the snapshot data for each session.
|
|
146
|
+
- Ability to delete a single session or clear all sessions from local history (dev-only).
|
|
147
|
+
|
|
148
|
+
It automatically hides itself in production (`import.meta.env.PROD`).
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { FormSnapshotsDevtools } from "form-snapshots"
|
|
152
|
+
|
|
153
|
+
export function RootLayout() {
|
|
154
|
+
return (
|
|
155
|
+
<>
|
|
156
|
+
{/* your app UI */}
|
|
157
|
+
<FormSnapshotsDevtools />
|
|
158
|
+
</>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Listing history: `useFormSnapshotsList`
|
|
166
|
+
|
|
167
|
+
`useFormSnapshotsList` lets you build your own history UI for a given form.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { useFormSnapshotsList } from "form-snapshots"
|
|
171
|
+
|
|
172
|
+
export function RecentSubmissions() {
|
|
173
|
+
const { items, isLoading } = useFormSnapshotsList("contact-form", {
|
|
174
|
+
onlySubmitted: true,
|
|
175
|
+
onlySuccessful: true,
|
|
176
|
+
maxAgeMs: 24 * 60 * 60 * 1000, // last 24h
|
|
177
|
+
limit: 20,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
if (isLoading) return <p>Loading…</p>
|
|
181
|
+
if (!items.length) return <p>No recent submissions.</p>
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<ul>
|
|
185
|
+
{items.map((item) => (
|
|
186
|
+
<li key={item.id}>
|
|
187
|
+
#{item.id} – {new Date(item.updatedAt).toLocaleString()} – status{" "}
|
|
188
|
+
{item.statusCode ?? "n/a"}
|
|
189
|
+
</li>
|
|
190
|
+
))}
|
|
191
|
+
</ul>
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Adapters and advanced usage
|
|
199
|
+
|
|
200
|
+
The library also exports:
|
|
201
|
+
|
|
202
|
+
- `useRHFFormSnapshots` – an adapter for **react-hook-form**.
|
|
203
|
+
- `useObjectFormSnapshots` – for generic object/React-state based forms.
|
|
204
|
+
- `FormSnapshotTable` – a table component for rendering snapshot key/value data.
|
|
205
|
+
- `FormSnapshotsClient` and `FormSnapshotsStorage` interfaces – if you want to plug in a custom storage backend instead of Dexie.
|
|
206
|
+
|
|
207
|
+
These build on the same concepts as `useFormSnapshots` but integrate more tightly with specific form/state libraries.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Building
|
|
212
|
+
|
|
213
|
+
This package is built with **tsup** and TypeScript:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run build
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The build outputs ESM, CJS, and `.d.ts` type declarations under `dist/`.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
ISC
|
|
226
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { SyntheticEvent, ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { UseFormReturn } from 'react-hook-form';
|
|
5
|
+
import { Dexie, EntityTable } from 'dexie';
|
|
6
|
+
|
|
7
|
+
type FormSnapshot = Record<string, unknown>;
|
|
8
|
+
interface FormSessionBase {
|
|
9
|
+
id: number;
|
|
10
|
+
formName: string;
|
|
11
|
+
data: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
submitted: boolean;
|
|
15
|
+
statusCode?: number | null;
|
|
16
|
+
errorMessage?: string | null;
|
|
17
|
+
}
|
|
18
|
+
interface FormSnapshotsStorage {
|
|
19
|
+
findActiveSession(formName: string): Promise<FormSessionBase | null>;
|
|
20
|
+
createSession(formName: string, snapshot: FormSnapshot): Promise<FormSessionBase>;
|
|
21
|
+
updateSession(id: number, patch: Partial<Pick<FormSessionBase, "data" | "updatedAt" | "submitted" | "statusCode" | "errorMessage">>): Promise<void>;
|
|
22
|
+
getLatestSession(formName: string): Promise<FormSessionBase | null>;
|
|
23
|
+
listSessions(formName: string): Promise<FormSessionBase[]>;
|
|
24
|
+
pruneOlderThan(cutoffMs: number): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Optional hook for storage backends that support deleting a session
|
|
27
|
+
* entirely. When implemented, it is used by features that discard
|
|
28
|
+
* submitted sessions instead of keeping them in history.
|
|
29
|
+
*/
|
|
30
|
+
deleteSession?(id: number): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FormSnapshotsOptions {
|
|
34
|
+
/** How long in ms to retain history entries. Default: 24 h */
|
|
35
|
+
snapshotsLimit?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Field paths (path/prefix) to exclude while saving snapshots.
|
|
38
|
+
* E.g. "password", "patient.address".
|
|
39
|
+
* Merged with the global excludeFields config.
|
|
40
|
+
*/
|
|
41
|
+
excludeFields?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Override how the current form state is read.
|
|
44
|
+
*
|
|
45
|
+
* Use this for controlled forms or nested / structured data models
|
|
46
|
+
* (e.g. HL7 FHIR Address, HumanName, ContactPoint[]) where values live
|
|
47
|
+
* in React state rather than plain DOM elements.
|
|
48
|
+
*/
|
|
49
|
+
getValues?: () => FormSnapshot;
|
|
50
|
+
/**
|
|
51
|
+
* Override how a saved snapshot is applied back to the form.
|
|
52
|
+
*
|
|
53
|
+
* Receives the full snapshot that was previously returned by `getValues`.
|
|
54
|
+
*/
|
|
55
|
+
applyValues?: (snapshot: FormSnapshot) => void;
|
|
56
|
+
/**
|
|
57
|
+
* When true, the active session is deleted from storage and the form
|
|
58
|
+
* is cleared immediately after submit so that submitted data is not
|
|
59
|
+
* kept in history. Can be overridden globally via provider config.
|
|
60
|
+
*/
|
|
61
|
+
discardOnSubmit?: boolean;
|
|
62
|
+
}
|
|
63
|
+
declare function useFormSnapshots(formName: string, options?: FormSnapshotsOptions): {
|
|
64
|
+
formRef: react.RefObject<HTMLFormElement>;
|
|
65
|
+
handleBlur: () => void;
|
|
66
|
+
wrapSubmit: (userSubmit?: (e: SyntheticEvent<HTMLFormElement>) => void) => (e: SyntheticEvent<HTMLFormElement>) => void;
|
|
67
|
+
wrapSubmitAsync: (userSubmit?: (e: SyntheticEvent<HTMLFormElement>) => Promise<void | {
|
|
68
|
+
statusCode?: number | null;
|
|
69
|
+
errorMessage?: string | null;
|
|
70
|
+
}>) => (e: SyntheticEvent<HTMLFormElement>) => Promise<void>;
|
|
71
|
+
restoreLatest: () => Promise<void>;
|
|
72
|
+
isSubmitted: boolean;
|
|
73
|
+
markSubmitResult: (params: {
|
|
74
|
+
statusCode?: number | null;
|
|
75
|
+
errorMessage?: string | null;
|
|
76
|
+
}) => Promise<void>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface useFormSnapshotsListOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Whether to return only submitted (completed) sessions.
|
|
82
|
+
* Default: false (both submitted and in-progress).
|
|
83
|
+
*/
|
|
84
|
+
onlySubmitted?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Whether to return only successful (2xx) submission results.
|
|
87
|
+
* Default: false (all statusCode values).
|
|
88
|
+
*/
|
|
89
|
+
onlySuccessful?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Maximum age in ms. E.g. last 24 hours = 24 * 60 * 60 * 1000.
|
|
92
|
+
* Default: unlimited.
|
|
93
|
+
*/
|
|
94
|
+
maxAgeMs?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Maximum number of records to return.
|
|
97
|
+
* Default: unlimited.
|
|
98
|
+
*/
|
|
99
|
+
limit?: number;
|
|
100
|
+
}
|
|
101
|
+
interface FormHistoryListItem {
|
|
102
|
+
id: number;
|
|
103
|
+
formName: string;
|
|
104
|
+
createdAt: number;
|
|
105
|
+
updatedAt: number;
|
|
106
|
+
submitted: boolean;
|
|
107
|
+
statusCode: number | null;
|
|
108
|
+
errorMessage: string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Age in ms relative to \"now\".
|
|
111
|
+
*/
|
|
112
|
+
ageMs: number;
|
|
113
|
+
}
|
|
114
|
+
declare function useFormSnapshotsList(formName: string, options?: useFormSnapshotsListOptions): {
|
|
115
|
+
items: FormHistoryListItem[];
|
|
116
|
+
isLoading: boolean;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
interface FormSnapshotsGlobalConfig {
|
|
120
|
+
defaultSnapshotsLimit?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Field paths (path/prefix) to exclude while saving snapshots.
|
|
123
|
+
* E.g. ["password", "patient.address"]
|
|
124
|
+
*/
|
|
125
|
+
excludeFields?: string[];
|
|
126
|
+
/**
|
|
127
|
+
* When true, submitted sessions are discarded and the form is cleared
|
|
128
|
+
* immediately after submit instead of keeping the data in history.
|
|
129
|
+
* Can be overridden per form via hook options.
|
|
130
|
+
*/
|
|
131
|
+
discardOnSubmit?: boolean;
|
|
132
|
+
}
|
|
133
|
+
interface FormSnapshotsProviderProps {
|
|
134
|
+
value?: FormSnapshotsGlobalConfig;
|
|
135
|
+
children: ReactNode;
|
|
136
|
+
}
|
|
137
|
+
declare function FormSnapshotsProvider({ value, children, }: Readonly<FormSnapshotsProviderProps>): react_jsx_runtime.JSX.Element;
|
|
138
|
+
declare function useFormSnapshotsConfig(): FormSnapshotsGlobalConfig;
|
|
139
|
+
|
|
140
|
+
declare function FormSnapshotsDevtools(): react_jsx_runtime.JSX.Element | null;
|
|
141
|
+
|
|
142
|
+
interface FormSnapshotTableProps {
|
|
143
|
+
/**
|
|
144
|
+
* JSON-stringified snapshot. Genellikle `FormSession.data`.
|
|
145
|
+
*/
|
|
146
|
+
data: string;
|
|
147
|
+
/**
|
|
148
|
+
* Message to display when the table is empty.
|
|
149
|
+
* Default: "No data captured yet."
|
|
150
|
+
*/
|
|
151
|
+
emptyMessage?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Extra class names for the outer wrapper.
|
|
154
|
+
*/
|
|
155
|
+
className?: string;
|
|
156
|
+
}
|
|
157
|
+
declare function FormSnapshotTable({ data, emptyMessage, className, }: Readonly<FormSnapshotTableProps>): react_jsx_runtime.JSX.Element;
|
|
158
|
+
|
|
159
|
+
type WithoutSnapshotOverrides = Omit<FormSnapshotsOptions, "getValues" | "applyValues">;
|
|
160
|
+
interface RHFSnapshotsOptions extends WithoutSnapshotOverrides {
|
|
161
|
+
}
|
|
162
|
+
declare function useRHFFormSnapshots<TFieldValues extends Record<string, unknown>>(formName: string, form: UseFormReturn<TFieldValues>, options?: RHFSnapshotsOptions): {
|
|
163
|
+
formRef: react.RefObject<HTMLFormElement>;
|
|
164
|
+
handleBlur: () => void;
|
|
165
|
+
wrapSubmit: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => void) => (e: react.SyntheticEvent<HTMLFormElement>) => void;
|
|
166
|
+
wrapSubmitAsync: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void | {
|
|
167
|
+
statusCode?: number | null;
|
|
168
|
+
errorMessage?: string | null;
|
|
169
|
+
}>) => (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void>;
|
|
170
|
+
restoreLatest: () => Promise<void>;
|
|
171
|
+
isSubmitted: boolean;
|
|
172
|
+
markSubmitResult: (params: {
|
|
173
|
+
statusCode?: number | null;
|
|
174
|
+
errorMessage?: string | null;
|
|
175
|
+
}) => Promise<void>;
|
|
176
|
+
};
|
|
177
|
+
interface ObjectStateSnapshotsOptions extends WithoutSnapshotOverrides {
|
|
178
|
+
}
|
|
179
|
+
declare function useObjectFormSnapshots<TState extends Record<string, unknown>>(formName: string, state: TState, setState: (next: TState) => void, options?: ObjectStateSnapshotsOptions): {
|
|
180
|
+
formRef: react.RefObject<HTMLFormElement>;
|
|
181
|
+
handleBlur: () => void;
|
|
182
|
+
wrapSubmit: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => void) => (e: react.SyntheticEvent<HTMLFormElement>) => void;
|
|
183
|
+
wrapSubmitAsync: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void | {
|
|
184
|
+
statusCode?: number | null;
|
|
185
|
+
errorMessage?: string | null;
|
|
186
|
+
}>) => (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void>;
|
|
187
|
+
restoreLatest: () => Promise<void>;
|
|
188
|
+
isSubmitted: boolean;
|
|
189
|
+
markSubmitResult: (params: {
|
|
190
|
+
statusCode?: number | null;
|
|
191
|
+
errorMessage?: string | null;
|
|
192
|
+
}) => Promise<void>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
declare class FormSnapshotsClient {
|
|
196
|
+
private readonly storage;
|
|
197
|
+
private readonly formName;
|
|
198
|
+
private readonly snapshotsLimit;
|
|
199
|
+
constructor(params: {
|
|
200
|
+
storage: FormSnapshotsStorage;
|
|
201
|
+
formName: string;
|
|
202
|
+
snapshotsLimit: number;
|
|
203
|
+
});
|
|
204
|
+
initSession(): Promise<FormSessionBase>;
|
|
205
|
+
saveSnapshot(sessionId: number, snapshot: FormSnapshot): Promise<void>;
|
|
206
|
+
markSubmitted(sessionId: number): Promise<void>;
|
|
207
|
+
deleteSession(sessionId: number): Promise<void>;
|
|
208
|
+
setSubmissionResult(params: {
|
|
209
|
+
sessionId: number;
|
|
210
|
+
statusCode?: number | null;
|
|
211
|
+
errorMessage?: string | null;
|
|
212
|
+
}): Promise<void>;
|
|
213
|
+
getLatestSnapshot(): Promise<FormSnapshot | null>;
|
|
214
|
+
openNewSessionFromSnapshot(snapshot: FormSnapshot): Promise<FormSessionBase>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface FormSession {
|
|
218
|
+
id: number;
|
|
219
|
+
formName: string;
|
|
220
|
+
/** JSON-serialised record of field name → value */
|
|
221
|
+
data: string;
|
|
222
|
+
createdAt: number;
|
|
223
|
+
updatedAt: number;
|
|
224
|
+
/** true = the form was submitted; session is closed / read-only */
|
|
225
|
+
submitted: boolean;
|
|
226
|
+
/** Optional HTTP-like status code for the last submit attempt */
|
|
227
|
+
statusCode?: number | null;
|
|
228
|
+
/** Optional error message when the last submit attempt failed */
|
|
229
|
+
errorMessage?: string | null;
|
|
230
|
+
}
|
|
231
|
+
declare const db: Dexie & {
|
|
232
|
+
formSessions: EntityTable<FormSession, "id">;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export { type FormHistoryListItem, type FormSession, type FormSessionBase, type FormSnapshot, FormSnapshotTable, type FormSnapshotTableProps, FormSnapshotsClient, FormSnapshotsDevtools, type FormSnapshotsOptions, FormSnapshotsProvider, type FormSnapshotsProviderProps, type FormSnapshotsStorage, type ObjectStateSnapshotsOptions, type RHFSnapshotsOptions, db, useFormSnapshots, useFormSnapshotsConfig, useFormSnapshotsList, type useFormSnapshotsListOptions, useObjectFormSnapshots, useRHFFormSnapshots };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { SyntheticEvent, ReactNode } from 'react';
|
|
3
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
4
|
+
import { UseFormReturn } from 'react-hook-form';
|
|
5
|
+
import { Dexie, EntityTable } from 'dexie';
|
|
6
|
+
|
|
7
|
+
type FormSnapshot = Record<string, unknown>;
|
|
8
|
+
interface FormSessionBase {
|
|
9
|
+
id: number;
|
|
10
|
+
formName: string;
|
|
11
|
+
data: string;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
submitted: boolean;
|
|
15
|
+
statusCode?: number | null;
|
|
16
|
+
errorMessage?: string | null;
|
|
17
|
+
}
|
|
18
|
+
interface FormSnapshotsStorage {
|
|
19
|
+
findActiveSession(formName: string): Promise<FormSessionBase | null>;
|
|
20
|
+
createSession(formName: string, snapshot: FormSnapshot): Promise<FormSessionBase>;
|
|
21
|
+
updateSession(id: number, patch: Partial<Pick<FormSessionBase, "data" | "updatedAt" | "submitted" | "statusCode" | "errorMessage">>): Promise<void>;
|
|
22
|
+
getLatestSession(formName: string): Promise<FormSessionBase | null>;
|
|
23
|
+
listSessions(formName: string): Promise<FormSessionBase[]>;
|
|
24
|
+
pruneOlderThan(cutoffMs: number): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Optional hook for storage backends that support deleting a session
|
|
27
|
+
* entirely. When implemented, it is used by features that discard
|
|
28
|
+
* submitted sessions instead of keeping them in history.
|
|
29
|
+
*/
|
|
30
|
+
deleteSession?(id: number): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface FormSnapshotsOptions {
|
|
34
|
+
/** How long in ms to retain history entries. Default: 24 h */
|
|
35
|
+
snapshotsLimit?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Field paths (path/prefix) to exclude while saving snapshots.
|
|
38
|
+
* E.g. "password", "patient.address".
|
|
39
|
+
* Merged with the global excludeFields config.
|
|
40
|
+
*/
|
|
41
|
+
excludeFields?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Override how the current form state is read.
|
|
44
|
+
*
|
|
45
|
+
* Use this for controlled forms or nested / structured data models
|
|
46
|
+
* (e.g. HL7 FHIR Address, HumanName, ContactPoint[]) where values live
|
|
47
|
+
* in React state rather than plain DOM elements.
|
|
48
|
+
*/
|
|
49
|
+
getValues?: () => FormSnapshot;
|
|
50
|
+
/**
|
|
51
|
+
* Override how a saved snapshot is applied back to the form.
|
|
52
|
+
*
|
|
53
|
+
* Receives the full snapshot that was previously returned by `getValues`.
|
|
54
|
+
*/
|
|
55
|
+
applyValues?: (snapshot: FormSnapshot) => void;
|
|
56
|
+
/**
|
|
57
|
+
* When true, the active session is deleted from storage and the form
|
|
58
|
+
* is cleared immediately after submit so that submitted data is not
|
|
59
|
+
* kept in history. Can be overridden globally via provider config.
|
|
60
|
+
*/
|
|
61
|
+
discardOnSubmit?: boolean;
|
|
62
|
+
}
|
|
63
|
+
declare function useFormSnapshots(formName: string, options?: FormSnapshotsOptions): {
|
|
64
|
+
formRef: react.RefObject<HTMLFormElement>;
|
|
65
|
+
handleBlur: () => void;
|
|
66
|
+
wrapSubmit: (userSubmit?: (e: SyntheticEvent<HTMLFormElement>) => void) => (e: SyntheticEvent<HTMLFormElement>) => void;
|
|
67
|
+
wrapSubmitAsync: (userSubmit?: (e: SyntheticEvent<HTMLFormElement>) => Promise<void | {
|
|
68
|
+
statusCode?: number | null;
|
|
69
|
+
errorMessage?: string | null;
|
|
70
|
+
}>) => (e: SyntheticEvent<HTMLFormElement>) => Promise<void>;
|
|
71
|
+
restoreLatest: () => Promise<void>;
|
|
72
|
+
isSubmitted: boolean;
|
|
73
|
+
markSubmitResult: (params: {
|
|
74
|
+
statusCode?: number | null;
|
|
75
|
+
errorMessage?: string | null;
|
|
76
|
+
}) => Promise<void>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface useFormSnapshotsListOptions {
|
|
80
|
+
/**
|
|
81
|
+
* Whether to return only submitted (completed) sessions.
|
|
82
|
+
* Default: false (both submitted and in-progress).
|
|
83
|
+
*/
|
|
84
|
+
onlySubmitted?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Whether to return only successful (2xx) submission results.
|
|
87
|
+
* Default: false (all statusCode values).
|
|
88
|
+
*/
|
|
89
|
+
onlySuccessful?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Maximum age in ms. E.g. last 24 hours = 24 * 60 * 60 * 1000.
|
|
92
|
+
* Default: unlimited.
|
|
93
|
+
*/
|
|
94
|
+
maxAgeMs?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Maximum number of records to return.
|
|
97
|
+
* Default: unlimited.
|
|
98
|
+
*/
|
|
99
|
+
limit?: number;
|
|
100
|
+
}
|
|
101
|
+
interface FormHistoryListItem {
|
|
102
|
+
id: number;
|
|
103
|
+
formName: string;
|
|
104
|
+
createdAt: number;
|
|
105
|
+
updatedAt: number;
|
|
106
|
+
submitted: boolean;
|
|
107
|
+
statusCode: number | null;
|
|
108
|
+
errorMessage: string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Age in ms relative to \"now\".
|
|
111
|
+
*/
|
|
112
|
+
ageMs: number;
|
|
113
|
+
}
|
|
114
|
+
declare function useFormSnapshotsList(formName: string, options?: useFormSnapshotsListOptions): {
|
|
115
|
+
items: FormHistoryListItem[];
|
|
116
|
+
isLoading: boolean;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
interface FormSnapshotsGlobalConfig {
|
|
120
|
+
defaultSnapshotsLimit?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Field paths (path/prefix) to exclude while saving snapshots.
|
|
123
|
+
* E.g. ["password", "patient.address"]
|
|
124
|
+
*/
|
|
125
|
+
excludeFields?: string[];
|
|
126
|
+
/**
|
|
127
|
+
* When true, submitted sessions are discarded and the form is cleared
|
|
128
|
+
* immediately after submit instead of keeping the data in history.
|
|
129
|
+
* Can be overridden per form via hook options.
|
|
130
|
+
*/
|
|
131
|
+
discardOnSubmit?: boolean;
|
|
132
|
+
}
|
|
133
|
+
interface FormSnapshotsProviderProps {
|
|
134
|
+
value?: FormSnapshotsGlobalConfig;
|
|
135
|
+
children: ReactNode;
|
|
136
|
+
}
|
|
137
|
+
declare function FormSnapshotsProvider({ value, children, }: Readonly<FormSnapshotsProviderProps>): react_jsx_runtime.JSX.Element;
|
|
138
|
+
declare function useFormSnapshotsConfig(): FormSnapshotsGlobalConfig;
|
|
139
|
+
|
|
140
|
+
declare function FormSnapshotsDevtools(): react_jsx_runtime.JSX.Element | null;
|
|
141
|
+
|
|
142
|
+
interface FormSnapshotTableProps {
|
|
143
|
+
/**
|
|
144
|
+
* JSON-stringified snapshot. Genellikle `FormSession.data`.
|
|
145
|
+
*/
|
|
146
|
+
data: string;
|
|
147
|
+
/**
|
|
148
|
+
* Message to display when the table is empty.
|
|
149
|
+
* Default: "No data captured yet."
|
|
150
|
+
*/
|
|
151
|
+
emptyMessage?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Extra class names for the outer wrapper.
|
|
154
|
+
*/
|
|
155
|
+
className?: string;
|
|
156
|
+
}
|
|
157
|
+
declare function FormSnapshotTable({ data, emptyMessage, className, }: Readonly<FormSnapshotTableProps>): react_jsx_runtime.JSX.Element;
|
|
158
|
+
|
|
159
|
+
type WithoutSnapshotOverrides = Omit<FormSnapshotsOptions, "getValues" | "applyValues">;
|
|
160
|
+
interface RHFSnapshotsOptions extends WithoutSnapshotOverrides {
|
|
161
|
+
}
|
|
162
|
+
declare function useRHFFormSnapshots<TFieldValues extends Record<string, unknown>>(formName: string, form: UseFormReturn<TFieldValues>, options?: RHFSnapshotsOptions): {
|
|
163
|
+
formRef: react.RefObject<HTMLFormElement>;
|
|
164
|
+
handleBlur: () => void;
|
|
165
|
+
wrapSubmit: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => void) => (e: react.SyntheticEvent<HTMLFormElement>) => void;
|
|
166
|
+
wrapSubmitAsync: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void | {
|
|
167
|
+
statusCode?: number | null;
|
|
168
|
+
errorMessage?: string | null;
|
|
169
|
+
}>) => (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void>;
|
|
170
|
+
restoreLatest: () => Promise<void>;
|
|
171
|
+
isSubmitted: boolean;
|
|
172
|
+
markSubmitResult: (params: {
|
|
173
|
+
statusCode?: number | null;
|
|
174
|
+
errorMessage?: string | null;
|
|
175
|
+
}) => Promise<void>;
|
|
176
|
+
};
|
|
177
|
+
interface ObjectStateSnapshotsOptions extends WithoutSnapshotOverrides {
|
|
178
|
+
}
|
|
179
|
+
declare function useObjectFormSnapshots<TState extends Record<string, unknown>>(formName: string, state: TState, setState: (next: TState) => void, options?: ObjectStateSnapshotsOptions): {
|
|
180
|
+
formRef: react.RefObject<HTMLFormElement>;
|
|
181
|
+
handleBlur: () => void;
|
|
182
|
+
wrapSubmit: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => void) => (e: react.SyntheticEvent<HTMLFormElement>) => void;
|
|
183
|
+
wrapSubmitAsync: (userSubmit?: (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void | {
|
|
184
|
+
statusCode?: number | null;
|
|
185
|
+
errorMessage?: string | null;
|
|
186
|
+
}>) => (e: react.SyntheticEvent<HTMLFormElement>) => Promise<void>;
|
|
187
|
+
restoreLatest: () => Promise<void>;
|
|
188
|
+
isSubmitted: boolean;
|
|
189
|
+
markSubmitResult: (params: {
|
|
190
|
+
statusCode?: number | null;
|
|
191
|
+
errorMessage?: string | null;
|
|
192
|
+
}) => Promise<void>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
declare class FormSnapshotsClient {
|
|
196
|
+
private readonly storage;
|
|
197
|
+
private readonly formName;
|
|
198
|
+
private readonly snapshotsLimit;
|
|
199
|
+
constructor(params: {
|
|
200
|
+
storage: FormSnapshotsStorage;
|
|
201
|
+
formName: string;
|
|
202
|
+
snapshotsLimit: number;
|
|
203
|
+
});
|
|
204
|
+
initSession(): Promise<FormSessionBase>;
|
|
205
|
+
saveSnapshot(sessionId: number, snapshot: FormSnapshot): Promise<void>;
|
|
206
|
+
markSubmitted(sessionId: number): Promise<void>;
|
|
207
|
+
deleteSession(sessionId: number): Promise<void>;
|
|
208
|
+
setSubmissionResult(params: {
|
|
209
|
+
sessionId: number;
|
|
210
|
+
statusCode?: number | null;
|
|
211
|
+
errorMessage?: string | null;
|
|
212
|
+
}): Promise<void>;
|
|
213
|
+
getLatestSnapshot(): Promise<FormSnapshot | null>;
|
|
214
|
+
openNewSessionFromSnapshot(snapshot: FormSnapshot): Promise<FormSessionBase>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface FormSession {
|
|
218
|
+
id: number;
|
|
219
|
+
formName: string;
|
|
220
|
+
/** JSON-serialised record of field name → value */
|
|
221
|
+
data: string;
|
|
222
|
+
createdAt: number;
|
|
223
|
+
updatedAt: number;
|
|
224
|
+
/** true = the form was submitted; session is closed / read-only */
|
|
225
|
+
submitted: boolean;
|
|
226
|
+
/** Optional HTTP-like status code for the last submit attempt */
|
|
227
|
+
statusCode?: number | null;
|
|
228
|
+
/** Optional error message when the last submit attempt failed */
|
|
229
|
+
errorMessage?: string | null;
|
|
230
|
+
}
|
|
231
|
+
declare const db: Dexie & {
|
|
232
|
+
formSessions: EntityTable<FormSession, "id">;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export { type FormHistoryListItem, type FormSession, type FormSessionBase, type FormSnapshot, FormSnapshotTable, type FormSnapshotTableProps, FormSnapshotsClient, FormSnapshotsDevtools, type FormSnapshotsOptions, FormSnapshotsProvider, type FormSnapshotsProviderProps, type FormSnapshotsStorage, type ObjectStateSnapshotsOptions, type RHFSnapshotsOptions, db, useFormSnapshots, useFormSnapshotsConfig, useFormSnapshotsList, type useFormSnapshotsListOptions, useObjectFormSnapshots, useRHFFormSnapshots };
|