@molecule/app-react 1.8.0 → 1.10.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 +152 -11
- package/dist/hooks/useProfileForm.d.ts +77 -0
- package/dist/hooks/useProfileForm.d.ts.map +1 -0
- package/dist/hooks/useProfileForm.js +199 -0
- package/dist/hooks/useProfileForm.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-10-
|
|
6
|
+
Generated: 2026-10-23T14:56:06.342Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/app-react
|
|
@@ -15,12 +15,38 @@ core interfaces (auth, i18n, theme, routing, state, http, storage, logger,
|
|
|
15
15
|
chat, workspace, editor, preview), so framework-agnostic providers plug
|
|
16
16
|
into React idioms.
|
|
17
17
|
|
|
18
|
+
**Editable account/profile forms:** `useProfileForm` composes the bonded form
|
|
19
|
+
and auth clients. It hydrates asynchronous user data without erasing dirty
|
|
20
|
+
fields, uses session ownership independently of editable profile identity,
|
|
21
|
+
snapshots controlled values on native form submission, submits only
|
|
22
|
+
changed fields through an app-supplied adapter, and persists/publishes the
|
|
23
|
+
server's saved user. Use its `onSaved` callback to update other query caches
|
|
24
|
+
from that response before navigating. Configure the endpoint, method and JSON
|
|
25
|
+
envelope once in `AuthClientConfig`; no page-local session store is needed.
|
|
26
|
+
|
|
18
27
|
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
19
28
|
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
20
29
|
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
21
30
|
|
|
22
31
|
## Quick Start
|
|
23
32
|
|
|
33
|
+
```tsx
|
|
34
|
+
const profile = useProfileForm<User, { name: string; email: string }>({
|
|
35
|
+
defaultValues: { name: '', email: '' },
|
|
36
|
+
toValues: (user) => ({ name: user.name ?? '', email: user.email ?? '' }),
|
|
37
|
+
toPatch: (changes) => changes,
|
|
38
|
+
onSaved: async (user) => {
|
|
39
|
+
// Set/invalidate the app's profile query keys using the saved response.
|
|
40
|
+
// Navigate only after those writes complete.
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
<form {...profile.formProps}>
|
|
44
|
+
<input {...profile.register('name')} />
|
|
45
|
+
<input {...profile.register('email', { email: true })} type="email" />
|
|
46
|
+
<button type="submit" disabled={!profile.ready || profile.isSubmitting}>Save</button>
|
|
47
|
+
</form>
|
|
48
|
+
```
|
|
49
|
+
|
|
24
50
|
```tsx
|
|
25
51
|
import { MoleculeProvider, useAuth, useTheme, useTranslation } from '@molecule/app-react'
|
|
26
52
|
import { provider as stateProvider } from '@molecule/app-state-zustand'
|
|
@@ -141,6 +167,15 @@ interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShape<TRegi
|
|
|
141
167
|
* Gets the current user.
|
|
142
168
|
*/
|
|
143
169
|
getUser(): T | null;
|
|
170
|
+
/**
|
|
171
|
+
* Optional ownership marker for the current session. Compare values only within
|
|
172
|
+
* this client: replacing or ending a session changes it, including re-adopting
|
|
173
|
+
* the same token and user. Profile updates and same-session refreshes keep it.
|
|
174
|
+
* Consumers can use it to discard stale async work without treating editable
|
|
175
|
+
* user fields as an immutable account identifier. Providers without this
|
|
176
|
+
* capability require the consumer's own stable identity check.
|
|
177
|
+
*/
|
|
178
|
+
getSessionRevision?(): number;
|
|
144
179
|
/**
|
|
145
180
|
* Updates the cached user object (state + persistent storage) without
|
|
146
181
|
* hitting the network. Intended for local refreshes after a per-app
|
|
@@ -215,6 +250,9 @@ interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShape<TRegi
|
|
|
215
250
|
confirmPasswordReset(data: PasswordResetConfirm): Promise<void>;
|
|
216
251
|
/**
|
|
217
252
|
* Updates the current user's profile.
|
|
253
|
+
* Returns the saved response. If the session is explicitly replaced or ended
|
|
254
|
+
* while the request is pending, that response does not replace the current
|
|
255
|
+
* session's user or persisted cache.
|
|
218
256
|
*/
|
|
219
257
|
updateProfile(data: Partial<T>): Promise<T>;
|
|
220
258
|
/**
|
|
@@ -273,16 +311,6 @@ interface AuthClient<T = UserProfile, TRegister extends RegisterExtraShape<TRegi
|
|
|
273
311
|
* Resolves IMMEDIATELY when no restore is in flight: before `initialize()` is ever
|
|
274
312
|
* called, and after it has finished. A restore that failed resolves too (it settled the
|
|
275
313
|
* question), so an HTTP client can await this before every request without risking one
|
|
276
|
-
* held forever. That is what makes the fetch-before-restore race impossible: a page that
|
|
277
|
-
* fetches viewer-dependent data on mount waits here instead of going out anonymously
|
|
278
|
-
* (see `setAuthGate` in `@molecule/app-http`).
|
|
279
|
-
*
|
|
280
|
-
* Optional: a provider that restores no session has nothing to wait for. Without it, an
|
|
281
|
-
* HTTP client bonded to that provider sends first-paint requests unauthenticated.
|
|
282
|
-
*
|
|
283
|
-
* A provider whose restore sends a request THROUGH the gated HTTP client must send that
|
|
284
|
-
* request with `skipAuthGate: true` (`RequestConfig` in `@molecule/app-http`): the gate
|
|
285
|
-
* waits on this promise, so holding the restore's own request makes the gate wait on
|
|
286
314
|
// … truncated — the full declaration is in node_modules/@molecule/app-react/dist
|
|
287
315
|
```
|
|
288
316
|
|
|
@@ -940,6 +968,76 @@ interface UsePlatformResult {
|
|
|
940
968
|
}
|
|
941
969
|
```
|
|
942
970
|
|
|
971
|
+
#### `UseProfileFormOptions`
|
|
972
|
+
|
|
973
|
+
Configuration for a profile form; transport stays in the auth bond.
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
interface UseProfileFormOptions<TUser, TValues extends Record<string, unknown>> extends Omit<
|
|
977
|
+
UseFormOptions<TValues>,
|
|
978
|
+
'defaultValues'
|
|
979
|
+
> {
|
|
980
|
+
/** Complete controlled-field defaults, including blank write-only fields. */
|
|
981
|
+
defaultValues: TValues
|
|
982
|
+
/**
|
|
983
|
+
* Optional additional account identity check; changing it discards prior edits.
|
|
984
|
+
* By default ownership follows the auth client's session revision, so editable
|
|
985
|
+
* fields can change during a save. Providers without getSessionRevision must
|
|
986
|
+
* supply this callback with an identity that remains stable across profile edits.
|
|
987
|
+
*/
|
|
988
|
+
getIdentity?: (user: TUser) => string
|
|
989
|
+
/** Maps the current/saved user into controlled values, e.g. nullable text to ''. */
|
|
990
|
+
toValues: (user: TUser) => TValues
|
|
991
|
+
/**
|
|
992
|
+
* Maps ONLY changed fields into an API patch. Omit blank optional credentials
|
|
993
|
+
* here; preserve undefined/absent fields rather than turning them into null.
|
|
994
|
+
* The second argument is the complete value snapshot taken at submit time.
|
|
995
|
+
*/
|
|
996
|
+
toPatch: (changes: Partial<TValues>, values: Readonly<TValues>) => Partial<TUser>
|
|
997
|
+
/**
|
|
998
|
+
* Propagates the server's saved user to other caches, then navigates if needed.
|
|
999
|
+
* Awaited before submission finishes. The auth client already persists and
|
|
1000
|
+
* publishes the saved user; never seed caches from the submitted patch.
|
|
1001
|
+
*/
|
|
1002
|
+
onSaved?: (user: TUser) => void | Promise<void>
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
#### `UseProfileFormResult`
|
|
1007
|
+
|
|
1008
|
+
Controlled profile form state and native form bindings.
|
|
1009
|
+
|
|
1010
|
+
```typescript
|
|
1011
|
+
interface UseProfileFormResult<TUser, TValues extends Record<string, unknown>> extends Omit<
|
|
1012
|
+
UseFormResult<TValues>,
|
|
1013
|
+
'register' | 'handleSubmit' | 'reset'
|
|
1014
|
+
> {
|
|
1015
|
+
/** The shared auth user's current profile. */
|
|
1016
|
+
user: TUser | null
|
|
1017
|
+
/** A user has been loaded; disable submission until this is true. */
|
|
1018
|
+
ready: boolean
|
|
1019
|
+
/**
|
|
1020
|
+
* The latest save/propagation failure. Rejected saves retain edits; a failed
|
|
1021
|
+
* onSaved callback can be retried by submit without repeating the mutation.
|
|
1022
|
+
*/
|
|
1023
|
+
error: unknown
|
|
1024
|
+
/** Native input/textarea bindings with the field's concrete value type. */
|
|
1025
|
+
register: <K extends keyof TValues>(
|
|
1026
|
+
name: K,
|
|
1027
|
+
options?: Parameters<UseFormResult<TValues>['register']>[1],
|
|
1028
|
+
) => Omit<FieldRegistration, 'value'> & { value: TValues[K] }
|
|
1029
|
+
/** Bind to the native form, with a type="submit" button inside it. */
|
|
1030
|
+
formProps: { onSubmit: (event: React.FormEvent<HTMLFormElement>) => void }
|
|
1031
|
+
/**
|
|
1032
|
+
* Validates and saves one submit-time snapshot. Concurrent submits share the
|
|
1033
|
+
* same promise. Resolves undefined on validation, save or propagation failure.
|
|
1034
|
+
*/
|
|
1035
|
+
submit: () => Promise<TUser | undefined>
|
|
1036
|
+
/** Discards edits and restores the newest loaded/saved profile baseline. */
|
|
1037
|
+
reset: () => void
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
943
1041
|
#### `UsePromiseState`
|
|
944
1042
|
|
|
945
1043
|
Extended promise state with actions.
|
|
@@ -2239,6 +2337,29 @@ function usePreview(): UsePreviewResult
|
|
|
2239
2337
|
|
|
2240
2338
|
**Returns:** Preview state and controls: state (url, isLoading, device, error, isConnected), setUrl, refresh, setDevice, and openExternal.
|
|
2241
2339
|
|
|
2340
|
+
#### `useProfileForm(options)`
|
|
2341
|
+
|
|
2342
|
+
Owns load → edit → submit → publish for the signed-in user's profile.
|
|
2343
|
+
|
|
2344
|
+
Same-account background updates refresh untouched fields but preserve dirty
|
|
2345
|
+
controls. Session ownership comes from the auth client, independently of mutable
|
|
2346
|
+
profile fields; an explicit getIdentity adds the caller's identity check.
|
|
2347
|
+
Submission snapshots the registered values before asynchronous
|
|
2348
|
+
validation, sends only changed fields through `toPatch`, and uses the auth
|
|
2349
|
+
client's configured updateProfile transport. The saved server response becomes
|
|
2350
|
+
the clean baseline; edits made while saving remain dirty. No request is made
|
|
2351
|
+
for a patch with no fields. A failed request retains the edits and shared user.
|
|
2352
|
+
|
|
2353
|
+
```typescript
|
|
2354
|
+
function useProfileForm(
|
|
2355
|
+
options: UseProfileFormOptions<TUser, TValues>,
|
|
2356
|
+
): UseProfileFormResult<TUser, TValues>
|
|
2357
|
+
```
|
|
2358
|
+
|
|
2359
|
+
- `options` — Controlled defaults, identity and wire adapters, optional cache/navigation callback.
|
|
2360
|
+
|
|
2361
|
+
**Returns:** Form state, typed control bindings, native submit binding and save error.
|
|
2362
|
+
|
|
2242
2363
|
#### `usePromise(asyncFn)`
|
|
2243
2364
|
|
|
2244
2365
|
Hook that wraps an async function with state tracking.
|
|
@@ -3028,6 +3149,26 @@ Peer dependencies:
|
|
|
3028
3149
|
- `@molecule/app-version`
|
|
3029
3150
|
- `react`
|
|
3030
3151
|
|
|
3152
|
+
- `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
|
|
3153
|
+
and uses the bonded forms provider, just like `useForm`. `defaultValues`
|
|
3154
|
+
and `toValues` must cover every registered control; map nullable text to
|
|
3155
|
+
`''` there. Add optional write-only controls such as `password: ''` to both,
|
|
3156
|
+
then omit a blank password in `toPatch`: `({ password, ...changes }) =>
|
|
3157
|
+
({ ...changes, ...(password?.trim() ? { password } : {}) })`. `changes`
|
|
3158
|
+
contains only edited fields, so do not fill omitted fields with null or
|
|
3159
|
+
copy every property of the auth user into the request. `reset()` discards
|
|
3160
|
+
edits to the newest baseline. Auth clients with `getSessionRevision()` own
|
|
3161
|
+
the form by session, so changing a username-derived id does not discard a
|
|
3162
|
+
successful save. An optional `getIdentity(user)` adds an identity check and
|
|
3163
|
+
clears edits when it changes; use a stable value. Providers without session
|
|
3164
|
+
revisions must supply that callback. Field comparisons are shallow, matching form providers.
|
|
3165
|
+
A submission whose account or auth client changes, or whose form unmounts,
|
|
3166
|
+
during asynchronous validation is discarded before any mutation or saved
|
|
3167
|
+
callback. Same-account background hydration does not discard the submit.
|
|
3168
|
+
The hook captures save failures in `error`; render that error with the
|
|
3169
|
+
app's normal API-error presentation. Use native `<form>` submit semantics,
|
|
3170
|
+
and pass the typed `register()` props through custom controls to their
|
|
3171
|
+
actual input/textarea elements.
|
|
3031
3172
|
- **Every hook throws when its provider is not mounted.** `MoleculeProvider` wires ONLY the
|
|
3032
3173
|
services you pass as props — it is a convenience wrapper, not a default registry. The map:
|
|
3033
3174
|
`useAuth`→`auth`, `useTranslation`/`useT`→`i18n`, `useTheme`→`theme`, `useRouter`→`router`,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile editing over the app's bonded form and auth clients.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { FieldRegistration } from '@molecule/app-forms';
|
|
7
|
+
import { type UseFormOptions, type UseFormResult } from './useForm.js';
|
|
8
|
+
/** Configuration for a profile form; transport stays in the auth bond. */
|
|
9
|
+
export interface UseProfileFormOptions<TUser, TValues extends Record<string, unknown>> extends Omit<UseFormOptions<TValues>, 'defaultValues'> {
|
|
10
|
+
/** Complete controlled-field defaults, including blank write-only fields. */
|
|
11
|
+
defaultValues: TValues;
|
|
12
|
+
/**
|
|
13
|
+
* Optional additional account identity check; changing it discards prior edits.
|
|
14
|
+
* By default ownership follows the auth client's session revision, so editable
|
|
15
|
+
* fields can change during a save. Providers without getSessionRevision must
|
|
16
|
+
* supply this callback with an identity that remains stable across profile edits.
|
|
17
|
+
*/
|
|
18
|
+
getIdentity?: (user: TUser) => string;
|
|
19
|
+
/** Maps the current/saved user into controlled values, e.g. nullable text to ''. */
|
|
20
|
+
toValues: (user: TUser) => TValues;
|
|
21
|
+
/**
|
|
22
|
+
* Maps ONLY changed fields into an API patch. Omit blank optional credentials
|
|
23
|
+
* here; preserve undefined/absent fields rather than turning them into null.
|
|
24
|
+
* The second argument is the complete value snapshot taken at submit time.
|
|
25
|
+
*/
|
|
26
|
+
toPatch: (changes: Partial<TValues>, values: Readonly<TValues>) => Partial<TUser>;
|
|
27
|
+
/**
|
|
28
|
+
* Propagates the server's saved user to other caches, then navigates if needed.
|
|
29
|
+
* Awaited before submission finishes. The auth client already persists and
|
|
30
|
+
* publishes the saved user; never seed caches from the submitted patch.
|
|
31
|
+
*/
|
|
32
|
+
onSaved?: (user: TUser) => void | Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
/** Controlled profile form state and native form bindings. */
|
|
35
|
+
export interface UseProfileFormResult<TUser, TValues extends Record<string, unknown>> extends Omit<UseFormResult<TValues>, 'register' | 'handleSubmit' | 'reset'> {
|
|
36
|
+
/** The shared auth user's current profile. */
|
|
37
|
+
user: TUser | null;
|
|
38
|
+
/** A user has been loaded; disable submission until this is true. */
|
|
39
|
+
ready: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The latest save/propagation failure. Rejected saves retain edits; a failed
|
|
42
|
+
* onSaved callback can be retried by submit without repeating the mutation.
|
|
43
|
+
*/
|
|
44
|
+
error: unknown;
|
|
45
|
+
/** Native input/textarea bindings with the field's concrete value type. */
|
|
46
|
+
register: <K extends keyof TValues>(name: K, options?: Parameters<UseFormResult<TValues>['register']>[1]) => Omit<FieldRegistration, 'value'> & {
|
|
47
|
+
value: TValues[K];
|
|
48
|
+
};
|
|
49
|
+
/** Bind to the native form, with a type="submit" button inside it. */
|
|
50
|
+
formProps: {
|
|
51
|
+
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Validates and saves one submit-time snapshot. Concurrent submits share the
|
|
55
|
+
* same promise. Resolves undefined on validation, save or propagation failure.
|
|
56
|
+
*/
|
|
57
|
+
submit: () => Promise<TUser | undefined>;
|
|
58
|
+
/** Discards edits and restores the newest loaded/saved profile baseline. */
|
|
59
|
+
reset: () => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Owns load → edit → submit → publish for the signed-in user's profile.
|
|
63
|
+
*
|
|
64
|
+
* Same-account background updates refresh untouched fields but preserve dirty
|
|
65
|
+
* controls. Session ownership comes from the auth client, independently of mutable
|
|
66
|
+
* profile fields; an explicit getIdentity adds the caller's identity check.
|
|
67
|
+
* Submission snapshots the registered values before asynchronous
|
|
68
|
+
* validation, sends only changed fields through `toPatch`, and uses the auth
|
|
69
|
+
* client's configured updateProfile transport. The saved server response becomes
|
|
70
|
+
* the clean baseline; edits made while saving remain dirty. No request is made
|
|
71
|
+
* for a patch with no fields. A failed request retains the edits and shared user.
|
|
72
|
+
*
|
|
73
|
+
* @param options - Controlled defaults, identity and wire adapters, optional cache/navigation callback.
|
|
74
|
+
* @returns Form state, typed control bindings, native submit binding and save error.
|
|
75
|
+
*/
|
|
76
|
+
export declare function useProfileForm<TUser, TValues extends Record<string, unknown>>(options: UseProfileFormOptions<TUser, TValues>): UseProfileFormResult<TUser, TValues>;
|
|
77
|
+
//# sourceMappingURL=useProfileForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProfileForm.d.ts","sourceRoot":"","sources":["../../src/hooks/useProfileForm.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,EAAW,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAE/E,0EAA0E;AAC1E,MAAM,WAAW,qBAAqB,CAAC,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,IAAI,CACjG,cAAc,CAAC,OAAO,CAAC,EACvB,eAAe,CAChB;IACC,6EAA6E;IAC7E,aAAa,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,MAAM,CAAA;IACrC,oFAAoF;IACpF,QAAQ,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAA;IAClC;;;;OAIG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,KAAK,CAAC,CAAA;IACjF;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD;AAED,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB,CAAC,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAE,SAAQ,IAAI,CAChG,aAAa,CAAC,OAAO,CAAC,EACtB,UAAU,GAAG,cAAc,GAAG,OAAO,CACtC;IACC,8CAA8C;IAC9C,IAAI,EAAE,KAAK,GAAG,IAAI,CAAA;IAClB,qEAAqE;IACrE,KAAK,EAAE,OAAO,CAAA;IACd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAA;IACd,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,CAAC,SAAS,MAAM,OAAO,EAChC,IAAI,EAAE,CAAC,EACP,OAAO,CAAC,EAAE,UAAU,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,KACxD,IAAI,CAAC,iBAAiB,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;KAAE,CAAA;IAC7D,sEAAsE;IACtE,SAAS,EAAE;QAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAA;KAAE,CAAA;IAC1E;;;OAGG;IACH,MAAM,EAAE,MAAM,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAA;IACxC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3E,OAAO,EAAE,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,GAC7C,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAsMtC"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile editing over the app's bonded form and auth clients.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useInsertionEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
7
|
+
import { useAuth, useAuthClient } from './useAuth.js';
|
|
8
|
+
import { useForm } from './useForm.js';
|
|
9
|
+
/**
|
|
10
|
+
* Owns load → edit → submit → publish for the signed-in user's profile.
|
|
11
|
+
*
|
|
12
|
+
* Same-account background updates refresh untouched fields but preserve dirty
|
|
13
|
+
* controls. Session ownership comes from the auth client, independently of mutable
|
|
14
|
+
* profile fields; an explicit getIdentity adds the caller's identity check.
|
|
15
|
+
* Submission snapshots the registered values before asynchronous
|
|
16
|
+
* validation, sends only changed fields through `toPatch`, and uses the auth
|
|
17
|
+
* client's configured updateProfile transport. The saved server response becomes
|
|
18
|
+
* the clean baseline; edits made while saving remain dirty. No request is made
|
|
19
|
+
* for a patch with no fields. A failed request retains the edits and shared user.
|
|
20
|
+
*
|
|
21
|
+
* @param options - Controlled defaults, identity and wire adapters, optional cache/navigation callback.
|
|
22
|
+
* @returns Form state, typed control bindings, native submit binding and save error.
|
|
23
|
+
*/
|
|
24
|
+
export function useProfileForm(options) {
|
|
25
|
+
const { defaultValues, getIdentity: _identity, toValues: _values, toPatch: _patch, onSaved: _saved, ...formOptions } = options;
|
|
26
|
+
const form = useForm({ ...formOptions, defaultValues });
|
|
27
|
+
const client = useAuthClient();
|
|
28
|
+
const { user } = useAuth();
|
|
29
|
+
const [error, setError] = useState();
|
|
30
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
31
|
+
const optionsRef = useRef(options);
|
|
32
|
+
const committedClient = useRef(client);
|
|
33
|
+
const baseline = useRef({ ...defaultValues });
|
|
34
|
+
const owner = useRef(null);
|
|
35
|
+
const activeSave = useRef(null);
|
|
36
|
+
const pendingPropagation = useRef(undefined);
|
|
37
|
+
const mounted = useRef(false);
|
|
38
|
+
// Actions use committed configuration, never an abandoned concurrent render.
|
|
39
|
+
useInsertionEffect(() => {
|
|
40
|
+
optionsRef.current = options;
|
|
41
|
+
committedClient.current = client;
|
|
42
|
+
}, [options, client]);
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
mounted.current = true;
|
|
45
|
+
return () => {
|
|
46
|
+
mounted.current = false;
|
|
47
|
+
};
|
|
48
|
+
}, []);
|
|
49
|
+
const readValues = useCallback(() => {
|
|
50
|
+
const values = { ...baseline.current };
|
|
51
|
+
for (const key of Object.keys(values))
|
|
52
|
+
values[key] = form.getValue(key);
|
|
53
|
+
return values;
|
|
54
|
+
}, [form.getValue]);
|
|
55
|
+
const rebase = useCallback((next, preserveAgainst) => {
|
|
56
|
+
const current = readValues();
|
|
57
|
+
baseline.current = { ...next };
|
|
58
|
+
form.reset(next);
|
|
59
|
+
if (preserveAgainst) {
|
|
60
|
+
for (const key of Object.keys(current)) {
|
|
61
|
+
if (!Object.is(current[key], preserveAgainst[key]))
|
|
62
|
+
form.setValue(key, current[key]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [readValues, form.reset, form.setValue]);
|
|
66
|
+
useLayoutEffect(() => {
|
|
67
|
+
const config = optionsRef.current;
|
|
68
|
+
if (!user) {
|
|
69
|
+
if (owner.current) {
|
|
70
|
+
owner.current = null;
|
|
71
|
+
pendingPropagation.current = undefined;
|
|
72
|
+
rebase({ ...config.defaultValues });
|
|
73
|
+
setError(undefined);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const revision = client.getSessionRevision?.();
|
|
78
|
+
const identity = config.getIdentity?.(user);
|
|
79
|
+
if (revision === undefined && !config.getIdentity) {
|
|
80
|
+
throw new Error('useProfileForm: provide getIdentity when the auth client has no session revision');
|
|
81
|
+
}
|
|
82
|
+
const previous = owner.current;
|
|
83
|
+
const sameAccount = !previous ||
|
|
84
|
+
(previous.client === client &&
|
|
85
|
+
previous.revision === revision &&
|
|
86
|
+
previous.identity === identity);
|
|
87
|
+
const previousBaseline = baseline.current;
|
|
88
|
+
// Keep this token across same-account hydration, but replace it when the
|
|
89
|
+
// account/client changes. A pending validation belongs to that exact owner.
|
|
90
|
+
if (!previous || !sameAccount)
|
|
91
|
+
owner.current = { client, revision, identity };
|
|
92
|
+
rebase(config.toValues(user), sameAccount ? previousBaseline : undefined);
|
|
93
|
+
if (!sameAccount) {
|
|
94
|
+
pendingPropagation.current = undefined;
|
|
95
|
+
setError(undefined);
|
|
96
|
+
}
|
|
97
|
+
}, [user, client, rebase]);
|
|
98
|
+
const submit = useCallback(() => {
|
|
99
|
+
if (activeSave.current)
|
|
100
|
+
return activeSave.current;
|
|
101
|
+
const currentUser = client.getUser();
|
|
102
|
+
if (!mounted.current || !currentUser)
|
|
103
|
+
return Promise.resolve(undefined);
|
|
104
|
+
const config = optionsRef.current;
|
|
105
|
+
const revision = client.getSessionRevision?.();
|
|
106
|
+
const identity = config.getIdentity?.(currentUser);
|
|
107
|
+
const submissionOwner = owner.current;
|
|
108
|
+
const ownsProfile = () => {
|
|
109
|
+
const current = client.getUser();
|
|
110
|
+
return (mounted.current &&
|
|
111
|
+
committedClient.current === client &&
|
|
112
|
+
owner.current === submissionOwner &&
|
|
113
|
+
current !== null &&
|
|
114
|
+
client.getSessionRevision?.() === revision &&
|
|
115
|
+
config.getIdentity?.(current) === identity);
|
|
116
|
+
};
|
|
117
|
+
const snapshot = readValues();
|
|
118
|
+
const before = { ...baseline.current };
|
|
119
|
+
const changes = {};
|
|
120
|
+
for (const key of Object.keys(snapshot)) {
|
|
121
|
+
if (!Object.is(snapshot[key], before[key]))
|
|
122
|
+
changes[key] = snapshot[key];
|
|
123
|
+
}
|
|
124
|
+
setError(undefined);
|
|
125
|
+
setIsSaving(true);
|
|
126
|
+
let saved;
|
|
127
|
+
const run = async () => {
|
|
128
|
+
try {
|
|
129
|
+
await form.handleSubmit(async () => {
|
|
130
|
+
// Validation can await arbitrary app work. Check BEFORE toPatch and
|
|
131
|
+
// again immediately before the mutation: its credentials are read by
|
|
132
|
+
// updateProfile now, not when the reader originally clicked Save.
|
|
133
|
+
if (!ownsProfile())
|
|
134
|
+
return;
|
|
135
|
+
const patch = config.toPatch(changes, snapshot);
|
|
136
|
+
if (Object.keys(patch).length > 0) {
|
|
137
|
+
if (!ownsProfile())
|
|
138
|
+
return;
|
|
139
|
+
saved = await client.updateProfile(patch);
|
|
140
|
+
if (!ownsProfile()) {
|
|
141
|
+
saved = undefined;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (mounted.current)
|
|
145
|
+
rebase(config.toValues(saved), snapshot);
|
|
146
|
+
pendingPropagation.current = saved;
|
|
147
|
+
}
|
|
148
|
+
if (pendingPropagation.current !== undefined) {
|
|
149
|
+
if (!ownsProfile()) {
|
|
150
|
+
saved = undefined;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const propagated = pendingPropagation.current;
|
|
154
|
+
await config.onSaved?.(propagated);
|
|
155
|
+
pendingPropagation.current = undefined;
|
|
156
|
+
saved = propagated;
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
return saved;
|
|
160
|
+
}
|
|
161
|
+
catch (failure) {
|
|
162
|
+
if (mounted.current)
|
|
163
|
+
setError(failure);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
activeSave.current = null;
|
|
168
|
+
if (mounted.current)
|
|
169
|
+
setIsSaving(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
activeSave.current = run();
|
|
173
|
+
return activeSave.current;
|
|
174
|
+
}, [client, readValues, form.handleSubmit, rebase]);
|
|
175
|
+
const register = useCallback((name, registerOptions) => {
|
|
176
|
+
return form.register(name, registerOptions);
|
|
177
|
+
}, [form.register]);
|
|
178
|
+
const reset = useCallback(() => {
|
|
179
|
+
form.reset(baseline.current);
|
|
180
|
+
setError(undefined);
|
|
181
|
+
}, [form.reset]);
|
|
182
|
+
const onSubmit = useCallback((event) => {
|
|
183
|
+
event.preventDefault();
|
|
184
|
+
void submit();
|
|
185
|
+
}, [submit]);
|
|
186
|
+
return {
|
|
187
|
+
...form,
|
|
188
|
+
formState: { ...form.formState, isSubmitting: isSaving || form.isSubmitting },
|
|
189
|
+
isSubmitting: isSaving || form.isSubmitting,
|
|
190
|
+
user,
|
|
191
|
+
ready: user !== null,
|
|
192
|
+
error,
|
|
193
|
+
register,
|
|
194
|
+
reset,
|
|
195
|
+
submit,
|
|
196
|
+
formProps: { onSubmit },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=useProfileForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useProfileForm.js","sourceRoot":"","sources":["../../src/hooks/useProfileForm.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,33 @@
|
|
|
6
6
|
* chat, workspace, editor, preview), so framework-agnostic providers plug
|
|
7
7
|
* into React idioms.
|
|
8
8
|
*
|
|
9
|
+
* **Editable account/profile forms:** `useProfileForm` composes the bonded form
|
|
10
|
+
* and auth clients. It hydrates asynchronous user data without erasing dirty
|
|
11
|
+
* fields, uses session ownership independently of editable profile identity,
|
|
12
|
+
* snapshots controlled values on native form submission, submits only
|
|
13
|
+
* changed fields through an app-supplied adapter, and persists/publishes the
|
|
14
|
+
* server's saved user. Use its `onSaved` callback to update other query caches
|
|
15
|
+
* from that response before navigating. Configure the endpoint, method and JSON
|
|
16
|
+
* envelope once in `AuthClientConfig`; no page-local session store is needed.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const profile = useProfileForm<User, { name: string; email: string }>({
|
|
21
|
+
* defaultValues: { name: '', email: '' },
|
|
22
|
+
* toValues: (user) => ({ name: user.name ?? '', email: user.email ?? '' }),
|
|
23
|
+
* toPatch: (changes) => changes,
|
|
24
|
+
* onSaved: async (user) => {
|
|
25
|
+
* // Set/invalidate the app's profile query keys using the saved response.
|
|
26
|
+
* // Navigate only after those writes complete.
|
|
27
|
+
* },
|
|
28
|
+
* })
|
|
29
|
+
* <form {...profile.formProps}>
|
|
30
|
+
* <input {...profile.register('name')} />
|
|
31
|
+
* <input {...profile.register('email', { email: true })} type="email" />
|
|
32
|
+
* <button type="submit" disabled={!profile.ready || profile.isSubmitting}>Save</button>
|
|
33
|
+
* </form>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
9
36
|
* @example
|
|
10
37
|
* ```tsx
|
|
11
38
|
* import { MoleculeProvider, useAuth, useTheme, useTranslation } from '@molecule/app-react'
|
|
@@ -48,6 +75,26 @@
|
|
|
48
75
|
* ```
|
|
49
76
|
*
|
|
50
77
|
* @remarks
|
|
78
|
+
* - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
|
|
79
|
+
* and uses the bonded forms provider, just like `useForm`. `defaultValues`
|
|
80
|
+
* and `toValues` must cover every registered control; map nullable text to
|
|
81
|
+
* `''` there. Add optional write-only controls such as `password: ''` to both,
|
|
82
|
+
* then omit a blank password in `toPatch`: `({ password, ...changes }) =>
|
|
83
|
+
* ({ ...changes, ...(password?.trim() ? { password } : {}) })`. `changes`
|
|
84
|
+
* contains only edited fields, so do not fill omitted fields with null or
|
|
85
|
+
* copy every property of the auth user into the request. `reset()` discards
|
|
86
|
+
* edits to the newest baseline. Auth clients with `getSessionRevision()` own
|
|
87
|
+
* the form by session, so changing a username-derived id does not discard a
|
|
88
|
+
* successful save. An optional `getIdentity(user)` adds an identity check and
|
|
89
|
+
* clears edits when it changes; use a stable value. Providers without session
|
|
90
|
+
* revisions must supply that callback. Field comparisons are shallow, matching form providers.
|
|
91
|
+
* A submission whose account or auth client changes, or whose form unmounts,
|
|
92
|
+
* during asynchronous validation is discarded before any mutation or saved
|
|
93
|
+
* callback. Same-account background hydration does not discard the submit.
|
|
94
|
+
* The hook captures save failures in `error`; render that error with the
|
|
95
|
+
* app's normal API-error presentation. Use native `<form>` submit semantics,
|
|
96
|
+
* and pass the typed `register()` props through custom controls to their
|
|
97
|
+
* actual input/textarea elements.
|
|
51
98
|
* - **Every hook throws when its provider is not mounted.** `MoleculeProvider` wires ONLY the
|
|
52
99
|
* services you pass as props — it is a convenience wrapper, not a default registry. The map:
|
|
53
100
|
* `useAuth`→`auth`, `useTranslation`/`useT`→`i18n`, `useTheme`→`theme`, `useRouter`→`router`,
|
|
@@ -104,6 +151,7 @@
|
|
|
104
151
|
export * from './agent-identity.js';
|
|
105
152
|
export * from './contexts.js';
|
|
106
153
|
export * from './hooks/index.js';
|
|
154
|
+
export * from './hooks/useProfileForm.js';
|
|
107
155
|
export * from './providers.js';
|
|
108
156
|
export * from './types.js';
|
|
109
157
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqJG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,33 @@
|
|
|
6
6
|
* chat, workspace, editor, preview), so framework-agnostic providers plug
|
|
7
7
|
* into React idioms.
|
|
8
8
|
*
|
|
9
|
+
* **Editable account/profile forms:** `useProfileForm` composes the bonded form
|
|
10
|
+
* and auth clients. It hydrates asynchronous user data without erasing dirty
|
|
11
|
+
* fields, uses session ownership independently of editable profile identity,
|
|
12
|
+
* snapshots controlled values on native form submission, submits only
|
|
13
|
+
* changed fields through an app-supplied adapter, and persists/publishes the
|
|
14
|
+
* server's saved user. Use its `onSaved` callback to update other query caches
|
|
15
|
+
* from that response before navigating. Configure the endpoint, method and JSON
|
|
16
|
+
* envelope once in `AuthClientConfig`; no page-local session store is needed.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const profile = useProfileForm<User, { name: string; email: string }>({
|
|
21
|
+
* defaultValues: { name: '', email: '' },
|
|
22
|
+
* toValues: (user) => ({ name: user.name ?? '', email: user.email ?? '' }),
|
|
23
|
+
* toPatch: (changes) => changes,
|
|
24
|
+
* onSaved: async (user) => {
|
|
25
|
+
* // Set/invalidate the app's profile query keys using the saved response.
|
|
26
|
+
* // Navigate only after those writes complete.
|
|
27
|
+
* },
|
|
28
|
+
* })
|
|
29
|
+
* <form {...profile.formProps}>
|
|
30
|
+
* <input {...profile.register('name')} />
|
|
31
|
+
* <input {...profile.register('email', { email: true })} type="email" />
|
|
32
|
+
* <button type="submit" disabled={!profile.ready || profile.isSubmitting}>Save</button>
|
|
33
|
+
* </form>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
9
36
|
* @example
|
|
10
37
|
* ```tsx
|
|
11
38
|
* import { MoleculeProvider, useAuth, useTheme, useTranslation } from '@molecule/app-react'
|
|
@@ -48,6 +75,26 @@
|
|
|
48
75
|
* ```
|
|
49
76
|
*
|
|
50
77
|
* @remarks
|
|
78
|
+
* - `useProfileForm` needs `AuthProvider` (or `MoleculeProvider auth={client}`)
|
|
79
|
+
* and uses the bonded forms provider, just like `useForm`. `defaultValues`
|
|
80
|
+
* and `toValues` must cover every registered control; map nullable text to
|
|
81
|
+
* `''` there. Add optional write-only controls such as `password: ''` to both,
|
|
82
|
+
* then omit a blank password in `toPatch`: `({ password, ...changes }) =>
|
|
83
|
+
* ({ ...changes, ...(password?.trim() ? { password } : {}) })`. `changes`
|
|
84
|
+
* contains only edited fields, so do not fill omitted fields with null or
|
|
85
|
+
* copy every property of the auth user into the request. `reset()` discards
|
|
86
|
+
* edits to the newest baseline. Auth clients with `getSessionRevision()` own
|
|
87
|
+
* the form by session, so changing a username-derived id does not discard a
|
|
88
|
+
* successful save. An optional `getIdentity(user)` adds an identity check and
|
|
89
|
+
* clears edits when it changes; use a stable value. Providers without session
|
|
90
|
+
* revisions must supply that callback. Field comparisons are shallow, matching form providers.
|
|
91
|
+
* A submission whose account or auth client changes, or whose form unmounts,
|
|
92
|
+
* during asynchronous validation is discarded before any mutation or saved
|
|
93
|
+
* callback. Same-account background hydration does not discard the submit.
|
|
94
|
+
* The hook captures save failures in `error`; render that error with the
|
|
95
|
+
* app's normal API-error presentation. Use native `<form>` submit semantics,
|
|
96
|
+
* and pass the typed `register()` props through custom controls to their
|
|
97
|
+
* actual input/textarea elements.
|
|
51
98
|
* - **Every hook throws when its provider is not mounted.** `MoleculeProvider` wires ONLY the
|
|
52
99
|
* services you pass as props — it is a convenience wrapper, not a default registry. The map:
|
|
53
100
|
* `useAuth`→`auth`, `useTranslation`/`useT`→`i18n`, `useTheme`→`theme`, `useRouter`→`router`,
|
|
@@ -104,6 +151,7 @@
|
|
|
104
151
|
export * from './agent-identity.js';
|
|
105
152
|
export * from './contexts.js';
|
|
106
153
|
export * from './hooks/index.js';
|
|
154
|
+
export * from './hooks/useProfileForm.js';
|
|
107
155
|
export * from './providers.js';
|
|
108
156
|
export * from './types.js';
|
|
109
157
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqJG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA;AAChC,cAAc,2BAA2B,CAAA;AACzC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/app-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "React framework bindings for molecule.dev",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@molecule/app-ai-models": "1.4.
|
|
61
|
+
"@molecule/app-ai-models": "1.4.2",
|
|
62
62
|
"@molecule/app-bonds-default-react": "1.1.5",
|
|
63
63
|
"@molecule/app-i18n": "1.0.3",
|
|
64
64
|
"@testing-library/dom": "10.4.1",
|