@alxia/react-router 0.23.0 → 0.24.1

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 CHANGED
@@ -1077,7 +1077,19 @@ export default function Users({ loaderData }: Route.ComponentProps) {
1077
1077
  }
1078
1078
  ```
1079
1079
 
1080
- The loader answers `?after=<cursor>` with the next page. A loader that throws on a cursor request lands on the route's `ErrorBoundary`: see [troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md). See the
1080
+ The loader answers `?after=<cursor>` with the next page. A loader that throws on a cursor request lands on the route's `ErrorBoundary`; wrap the read in `orFailed(after, run)` to keep the rows and offer a retry instead:
1081
+
1082
+ ```ts
1083
+ import { orFailed } from '@alxia/react-router/lists';
1084
+
1085
+ export function loader({ request }: Route.LoaderArgs) {
1086
+ const url = new URL(request.url);
1087
+ // no ?after: a failure throws (ErrorBoundary); with ?after: it answers { failed: true }
1088
+ return orFailed(url.searchParams.get('after'), () => listUsers(url.searchParams));
1089
+ }
1090
+ ```
1091
+
1092
+ `usePagedRows` reads `{ failed: true }` as a page without rows, so it sets `error` and keeps the rows. See [troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md). See the
1081
1093
  [lists guide](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide/lists.md).
1082
1094
 
1083
1095
  ## Traps
@@ -1289,9 +1301,10 @@ From `@alxia/react-router/lists` (`react` and `react-router` are its only import
1289
1301
 
1290
1302
  | export | |
1291
1303
  | --- | --- |
1292
- | `useSearchChange({ resetKeys? })`, `SearchChangeOptions` | `(changes) => void`: sets a parameter given a string, deletes one given `null` or `undefined`, and deletes `resetKeys` (default `['after']`); the history entry is replaced and the scroll position kept |
1304
+ | `useSearchChange({ resetKeys? })`, `SearchChange`, `SearchChangeOptions` | `(changes: SearchChange) => void`: a record sets a parameter given a string, deletes one given `null` or `undefined`, and deletes `resetKeys` (default `['after']`); a `URLSearchParams` replaces the whole search, and an updater `(prev) => URLSearchParams` computes it (`resetKeys` does not apply to these two); the history entry is replaced and the scroll position kept |
1293
1305
  | `useSameRouteLoading()` | `true` while a navigation is loading and its pathname is the current one (another search of the list), `false` for any other page |
1294
1306
  | `usePagedRows(firstPage, rowsOf, { cursorParam?, nextOf? }?)`, `PagedRows`, `PagedRowsOptions` | `{ rows, next, loading, error, loadMore }`: appends the pages the same route answers to `?<cursorParam>=<next>` (default `after`, `nextOf` default `page.next`) through `useFetcher`; starts over when `firstPage` is another object; a failure keeps the rows and sets `error` (an answer that is not a page), and data that is not a page is ignored |
1307
+ | `orFailed(after, run)`, `FAILED`, `isFailedPage(value)`, `FailedPage` | the loader half of `usePagedRows`: `run()`'s answer; an error is rethrown when `after` is empty, and with an `after` it answers `FAILED` (`{ failed: true }`, typed `Promise<T \| FailedPage>`), except a 3xx `Response`, which is rethrown. `isFailedPage` narrows an answer |
1295
1308
 
1296
1309
  From `@alxia/react-router/subscription` (no import, no peer):
1297
1310
 
@@ -11,7 +11,7 @@ export type CodeIssueLike = {
11
11
  readonly expected?: unknown;
12
12
  readonly message?: string | undefined;
13
13
  };
14
- /** An API error code (`<module>.errors.<slug>`), as opposed to a GraphQL one (`BAD_USER_INPUT`). */
14
+ /** An API error code (`<module>.errors.<slug>`, the module in kebab-case or camelCase), as opposed to a GraphQL one (`BAD_USER_INPUT`). */
15
15
  export declare const API_CODE: RegExp;
16
16
  export interface ApiCodeResolverOptions<L extends string> {
17
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"code-resolver.d.ts","sourceRoot":"","sources":["../../src/errors/code-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,CAAC;AAEF,oGAAoG;AACpG,eAAO,MAAM,QAAQ,QAAkC,CAAC;AAExD,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM;IACvD;;;OAGG;IACH,SAAS,CACR,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC3C,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GACtB,MAAM,CAAC;IACV,mDAAmD;IACnD,QAAQ,IAAI,CAAC,CAAC;IACd,yEAAyE;IACzE,MAAM,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;IAClD,yEAAyE;IACzE,SAAS,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,MAAM;IAChD,SAAS,EAAE,CACV,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC3C,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,KACpB,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAC;CACrD;AAOD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC/C,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EACxB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,eAAe,CAAC,CAAC,CAAC,CAiBpB"}
1
+ {"version":3,"file":"code-resolver.d.ts","sourceRoot":"","sources":["../../src/errors/code-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC,CAAC;AAEF,2IAA2I;AAC3I,eAAO,MAAM,QAAQ,QAA6C,CAAC;AAEnE,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,MAAM;IACvD;;;OAGG;IACH,SAAS,CACR,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC3C,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GACtB,MAAM,CAAC;IACV,mDAAmD;IACnD,QAAQ,IAAI,CAAC,CAAC;IACd,yEAAyE;IACzE,MAAM,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAAC;IAClD,yEAAyE;IACzE,SAAS,CAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,yEAAyE;AACzE,MAAM,WAAW,eAAe,CAAC,CAAC,SAAS,MAAM;IAChD,SAAS,EAAE,CACV,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC3C,QAAQ,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,KACpB,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAC;CACrD;AAOD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAC/C,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EACxB,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,eAAe,CAAC,CAAC,CAAC,CAiBpB"}
@@ -15,7 +15,7 @@ import {
15
15
  rememberSiteCheck
16
16
  } from "../chunks/index-rgy503w9.js";
17
17
  // src/errors/code-resolver.ts
18
- var API_CODE = /^[a-z-]+\.errors\.[a-z0-9-]+$/;
18
+ var API_CODE = /^[a-z][a-zA-Z0-9-]*\.errors\.[a-z0-9-]+$/;
19
19
  function paramsCode(issue) {
20
20
  const code = issue.params?.code;
21
21
  return typeof code === "string" ? code : undefined;
@@ -538,5 +538,5 @@ export {
538
538
  waitMinutes2 as waitMinutes
539
539
  };
540
540
 
541
- //# debugId=9CC6E221BA433AF564756E2164756E21
541
+ //# debugId=C040A51D1737CFBA64756E2164756E21
542
542
  //# sourceMappingURL=index.js.map
@@ -2,7 +2,7 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/errors/code-resolver.ts", "../src/errors/describe-refusal.ts", "../src/errors/fetcher-result.ts", "../src/errors/submit-form.ts", "../src/errors/describe.ts", "../src/errors/api-errors.ts", "../src/errors/log.ts", "../src/errors/call-failure.ts", "../src/errors/catalogue-check.ts", "../src/errors/catalogue-read.ts", "../src/errors/catalogue.ts", "../src/errors/json-actions.ts", "../src/errors/message-values.ts", "../src/errors/messages.ts", "../src/errors/provider.ts", "../src/errors/use-json-fetcher.ts"],
4
4
  "sourcesContent": [
5
- "import type { ApiMessages } from './messages';\n\n/** The part of a zod issue a code reads, by shape (`@nxgt/material-core`'s `CodeIssue`). */\nexport type CodeIssueLike = {\n\treadonly code: string;\n\t/** A custom issue's params: `.refine(…, { params: { code } })`. */\n\treadonly params?: Record<string, unknown> | undefined;\n\treadonly input?: unknown;\n\treadonly origin?: string | undefined;\n\treadonly minimum?: unknown;\n\treadonly maximum?: unknown;\n\treadonly expected?: unknown;\n\treadonly message?: string | undefined;\n};\n\n/** An API error code (`<module>.errors.<slug>`), as opposed to a GraphQL one (`BAD_USER_INPUT`). */\nexport const API_CODE = /^[a-z-]+\\.errors\\.[a-z0-9-]+$/;\n\nexport interface ApiCodeResolverOptions<L extends string> {\n\t/**\n\t * The app's translator, for every key that is not an API code. Method\n\t * syntax: the app's own `Translate<LocaleKey>` is accepted.\n\t */\n\ttranslate(\n\t\tkey: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t\tlanguage?: L | (() => L),\n\t): string;\n\t/** The page's language, when a call names none. */\n\tlanguage(): L;\n\t/** Which code an issue carries; the issue's `params.code` by default. */\n\tcodeOf?(issue: CodeIssueLike): string | undefined;\n\t/** Whether a key is an API code; `<module>.errors.<slug>` by default. */\n\tisApiCode?(key: string): boolean;\n}\n\n/** What `@nxgt/material`'s `useCodeResolver(schema, resolver)` takes. */\nexport interface ApiCodeResolver<L extends string> {\n\ttranslate: (\n\t\tkey: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t\tlanguage?: L | (() => L),\n\t) => string;\n\tcodeOf: (issue: CodeIssueLike) => string | undefined;\n}\n\nfunction paramsCode(issue: CodeIssueLike) {\n\tconst code = (issue.params as { code?: unknown } | undefined)?.code;\n\treturn typeof code === 'string' ? code : undefined;\n}\n\n/**\n * The words of what a form's checks ask for: a refinement carrying an API\n * code in `params.code` reads exactly as the API words it (from the\n * catalogue `messages` holds), any other key takes the app's translator, and\n * a code the catalogue lacks comes back as it is, so the form library words\n * the issue with its own locale instead. Both functions are stable: a\n * resolver is made once per schema and language, so build this once, at\n * module scope.\n */\nexport function apiCodeResolver<L extends string>(\n\tmessages: ApiMessages<L>,\n\toptions: ApiCodeResolverOptions<L>,\n): ApiCodeResolver<L> {\n\tconst isApiCode = (key: string) =>\n\t\toptions.isApiCode ? options.isApiCode(key) : API_CODE.test(key);\n\treturn {\n\t\ttranslate(key, context, provideLanguage) {\n\t\t\tif (!isApiCode(key))\n\t\t\t\treturn options.translate(key, context, provideLanguage);\n\t\t\tconst language =\n\t\t\t\ttypeof provideLanguage === 'function'\n\t\t\t\t\t? provideLanguage()\n\t\t\t\t\t: (provideLanguage ?? options.language());\n\t\t\treturn messages.apiMessage(key, context, language) ?? key;\n\t\t},\n\t\tcodeOf(issue) {\n\t\t\treturn options.codeOf ? options.codeOf(issue) : paramsCode(issue);\n\t\t},\n\t};\n}\n",
5
+ "import type { ApiMessages } from './messages';\n\n/** The part of a zod issue a code reads, by shape (`@nxgt/material-core`'s `CodeIssue`). */\nexport type CodeIssueLike = {\n\treadonly code: string;\n\t/** A custom issue's params: `.refine(…, { params: { code } })`. */\n\treadonly params?: Record<string, unknown> | undefined;\n\treadonly input?: unknown;\n\treadonly origin?: string | undefined;\n\treadonly minimum?: unknown;\n\treadonly maximum?: unknown;\n\treadonly expected?: unknown;\n\treadonly message?: string | undefined;\n};\n\n/** An API error code (`<module>.errors.<slug>`, the module in kebab-case or camelCase), as opposed to a GraphQL one (`BAD_USER_INPUT`). */\nexport const API_CODE = /^[a-z][a-zA-Z0-9-]*\\.errors\\.[a-z0-9-]+$/;\n\nexport interface ApiCodeResolverOptions<L extends string> {\n\t/**\n\t * The app's translator, for every key that is not an API code. Method\n\t * syntax: the app's own `Translate<LocaleKey>` is accepted.\n\t */\n\ttranslate(\n\t\tkey: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t\tlanguage?: L | (() => L),\n\t): string;\n\t/** The page's language, when a call names none. */\n\tlanguage(): L;\n\t/** Which code an issue carries; the issue's `params.code` by default. */\n\tcodeOf?(issue: CodeIssueLike): string | undefined;\n\t/** Whether a key is an API code; `<module>.errors.<slug>` by default. */\n\tisApiCode?(key: string): boolean;\n}\n\n/** What `@nxgt/material`'s `useCodeResolver(schema, resolver)` takes. */\nexport interface ApiCodeResolver<L extends string> {\n\ttranslate: (\n\t\tkey: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t\tlanguage?: L | (() => L),\n\t) => string;\n\tcodeOf: (issue: CodeIssueLike) => string | undefined;\n}\n\nfunction paramsCode(issue: CodeIssueLike) {\n\tconst code = (issue.params as { code?: unknown } | undefined)?.code;\n\treturn typeof code === 'string' ? code : undefined;\n}\n\n/**\n * The words of what a form's checks ask for: a refinement carrying an API\n * code in `params.code` reads exactly as the API words it (from the\n * catalogue `messages` holds), any other key takes the app's translator, and\n * a code the catalogue lacks comes back as it is, so the form library words\n * the issue with its own locale instead. Both functions are stable: a\n * resolver is made once per schema and language, so build this once, at\n * module scope.\n */\nexport function apiCodeResolver<L extends string>(\n\tmessages: ApiMessages<L>,\n\toptions: ApiCodeResolverOptions<L>,\n): ApiCodeResolver<L> {\n\tconst isApiCode = (key: string) =>\n\t\toptions.isApiCode ? options.isApiCode(key) : API_CODE.test(key);\n\treturn {\n\t\ttranslate(key, context, provideLanguage) {\n\t\t\tif (!isApiCode(key))\n\t\t\t\treturn options.translate(key, context, provideLanguage);\n\t\t\tconst language =\n\t\t\t\ttypeof provideLanguage === 'function'\n\t\t\t\t\t? provideLanguage()\n\t\t\t\t\t: (provideLanguage ?? options.language());\n\t\t\treturn messages.apiMessage(key, context, language) ?? key;\n\t\t},\n\t\tcodeOf(issue) {\n\t\t\treturn options.codeOf ? options.codeOf(issue) : paramsCode(issue);\n\t\t},\n\t};\n}\n",
6
6
  "import { isApiErrorLike } from './api-error';\nimport type { ApiErrors } from './api-errors';\n\n/** The API's refusal of `error`, or `undefined` when it is not one. */\nexport function refusalOf<L extends string>(\n\terrors: Pick<ApiErrors<L>, 'apiErrorMessage' | 'unavailableMessage'>,\n\terror: unknown,\n\tlanguage: L,\n): ReturnType<ApiErrors<L>['apiRefusal']> {\n\tif (isApiErrorLike(error)) {\n\t\treturn {\n\t\t\tmessage: errors.apiErrorMessage(error, language),\n\t\t\tstatus: error.status,\n\t\t};\n\t}\n\tif (error instanceof Response && error.status === 503) {\n\t\treturn { message: errors.unavailableMessage(language), status: 503 };\n\t}\n\treturn undefined;\n}\n\n/** The worded message of each field an API error names, by field path. */\nexport function fieldErrorsOf<L extends string>(\n\terrors: Pick<ApiErrors<L>, 'apiErrorMessage' | 'translateCode'>,\n\terror: Parameters<ApiErrors<L>['fieldErrors']>[0],\n\tlanguage: L,\n): ReturnType<ApiErrors<L>['fieldErrors']> {\n\tconst extensions = error.errors[0]?.extensions;\n\tconst fields = Array.isArray(extensions?.fields) ? extensions.fields : [];\n\tif (!fields.length && extensions?.field) {\n\t\treturn { [extensions.field]: errors.apiErrorMessage(error, language) };\n\t}\n\treturn Object.fromEntries(\n\t\tfields.map(({ path, code, message, params }) => [\n\t\t\tpath,\n\t\t\terrors.translateCode(code, message, language, params),\n\t\t]),\n\t);\n}\n",
7
7
  "import { data } from 'react-router';\nimport { isApiErrorLike } from './api-error';\nimport type { ApiErrors, ApiRefusal } from './api-errors';\nimport { type RouteErrorOptions, routeError } from './route-error';\n\n/**\n * A fetcher's refusal: the API's (`apiRefusal`), an unreachable API's too,\n * so the page stays where it was. A 401 (the session caller redirects\n * first) or any other failure is thrown, for the error page.\n */\nexport function fetcherRefusal<L extends string>(\n\terrors: Pick<ApiErrors<L>, 'apiRefusal'>,\n\terror: unknown,\n\tlanguage: L,\n\troute?: RouteErrorOptions,\n): ApiRefusal {\n\tconst refusal =\n\t\tisApiErrorLike(error) && error.status === 401\n\t\t\t? undefined\n\t\t\t: errors.apiRefusal(error, language);\n\tif (!refusal) throw routeError(error, route);\n\treturn refusal;\n}\n\n/**\n * Runs the mutation of an action a fetcher posted to: `{ ok: true }` with\n * `about` and the mutation's value, or the refusal (`fetcherRefusal`) as a\n * worded `error`, with its HTTP status, for a toast or a form message.\n */\nexport async function fetcherResult<\n\tL extends string,\n\tAbout extends object,\n\tValue extends object = Record<never, never>,\n>(\n\terrors: Pick<ApiErrors<L>, 'apiRefusal'>,\n\tlanguage: L,\n\tabout: About,\n\trun: () => Promise<Value>,\n\troute?: RouteErrorOptions,\n) {\n\ttry {\n\t\treturn { ok: true as const, ...about, ...(await run()) };\n\t} catch (error) {\n\t\tconst { message, status } = fetcherRefusal(errors, error, language, route);\n\t\treturn data({ ok: false as const, ...about, error: message }, { status });\n\t}\n}\n",
8
8
  "import { data } from 'react-router';\nimport { isApiErrorLike } from './api-error';\nimport type { ApiErrors } from './api-errors';\nimport type { FormErrors } from './form-errors';\nimport { type RouteErrorOptions, routeError } from './route-error';\n\n/**\n * Runs a form's mutation. An `ApiError` becomes the form's errors: each\n * refused field (`extensions.fields`, or the one `extensions.field` names)\n * under the form field `fieldOf` names (else on the form), and any other\n * code as the form's message. A 404 about none of the form's fields (the\n * record itself is gone), a 401 and other failures go to the error page.\n */\nexport async function submitForm<L extends string, T>(\n\terrors: Pick<ApiErrors<L>, 'apiErrorMessage' | 'fieldErrors'>,\n\tlanguage: L,\n\trun: () => Promise<T>,\n\tfieldOf: (path: string) => string | undefined,\n\troute?: RouteErrorOptions,\n): Promise<{ ok: true; value: T } | ReturnType<typeof data<FormErrors>>> {\n\ttry {\n\t\treturn { ok: true, value: await run() };\n\t} catch (error) {\n\t\tif (!isApiErrorLike(error) || error.status === 401) {\n\t\t\tthrow routeError(error, route);\n\t\t}\n\t\tconst field = error.errors[0]?.extensions?.field;\n\t\tif (error.status === 404 && !(field && fieldOf(field))) {\n\t\t\tthrow routeError(error, route);\n\t\t}\n\t\tconst byField: Record<string, string> = {};\n\t\tconst unplaced: string[] = [];\n\t\tfor (const [path, message] of Object.entries(\n\t\t\terrors.fieldErrors(error, language),\n\t\t)) {\n\t\t\tconst field = fieldOf(path);\n\t\t\tif (field) byField[field] = message;\n\t\t\telse unplaced.push(message);\n\t\t}\n\t\tconst formError =\n\t\t\tObject.keys(byField).length && !unplaced.length\n\t\t\t\t? undefined\n\t\t\t\t: [errors.apiErrorMessage(error, language), ...unplaced].join(' ');\n\t\treturn data({ fieldErrors: byField, formError }, { status: error.status });\n\t}\n}\n",
@@ -20,6 +20,6 @@
20
20
  "import { type FetcherWithComponents, useFetcher } from 'react-router';\n\n/** What a fetcher can post as JSON. */\nexport type JsonBody = Parameters<FetcherWithComponents<unknown>['submit']>[0];\n\n/**\n * A fetcher posting JSON to its page's action (which reads it with\n * `readJson`, and answers `fetcherResult` or the like): `post` sends a\n * body, `busy` while it is in flight. `key` shares the fetcher between the\n * form that posts and the component that reads the answer; `action` posts to\n * another route's action instead (a resource route's, such as a\n * `notifications/poll`). `post` takes the target of one call as its second\n * argument (a list's rows each post to their own record's action).\n */\nexport function useJsonFetcher<Action, Body extends JsonBody = JsonBody>(\n\tkey?: string,\n\taction?: string,\n): {\n\tfetcher: ReturnType<typeof useFetcher<Action>>;\n\tpost(body: Body, to?: string): Promise<void>;\n\tbusy: boolean;\n} {\n\tconst fetcher = useFetcher<Action>(key ? { key } : undefined);\n\tconst post = (body: Body, to = action) =>\n\t\tfetcher.submit(body, {\n\t\t\tmethod: 'post',\n\t\t\tencType: 'application/json',\n\t\t\t...(to ? { action: to } : {}),\n\t\t});\n\treturn { fetcher, post, busy: fetcher.state !== 'idle' };\n}\n"
21
21
  ],
22
22
  "mappings": "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",
23
- "debugId": "9CC6E221BA433AF564756E2164756E21",
23
+ "debugId": "C040A51D1737CFBA64756E2164756E21",
24
24
  "names": []
25
25
  }
@@ -3,7 +3,8 @@
3
3
  * URL (a search, filters, a cursor). `react` and `react-router` are its only
4
4
  * imports, and every hook renders on the server.
5
5
  */
6
+ export { FAILED, type FailedPage, isFailedPage, orFailed, } from './or-failed';
6
7
  export { DEFAULT_CURSOR_PARAM, type PagedRows, type PagedRowsOptions, usePagedRows, } from './use-paged-rows';
7
8
  export { useSameRouteLoading } from './use-same-route-loading';
8
- export { type SearchChangeOptions, useSearchChange } from './use-search-change';
9
+ export { type SearchChange, type SearchChangeOptions, useSearchChange, } from './use-search-change';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lists/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,oBAAoB,EACpB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,YAAY,GACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,KAAK,mBAAmB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lists/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,MAAM,EACN,KAAK,UAAU,EACf,YAAY,EACZ,QAAQ,GACR,MAAM,aAAa,CAAC;AACrB,OAAO,EACN,oBAAoB,EACpB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,YAAY,GACZ,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EACN,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,eAAe,GACf,MAAM,qBAAqB,CAAC"}
@@ -1,3 +1,19 @@
1
+ // src/lists/or-failed.ts
2
+ var FAILED = Object.freeze({ failed: true });
3
+ function isFailedPage(value) {
4
+ return typeof value === "object" && value !== null && value.failed === true;
5
+ }
6
+ async function orFailed(after, run) {
7
+ if (!after)
8
+ return await run();
9
+ try {
10
+ return await run();
11
+ } catch (error) {
12
+ if (error instanceof Response && error.status >= 300 && error.status < 400)
13
+ throw error;
14
+ return FAILED;
15
+ }
16
+ }
1
17
  // src/lists/use-paged-rows.ts
2
18
  import { useCallback, useEffect, useRef, useState } from "react";
3
19
  import { useFetcher, useLocation } from "react-router";
@@ -101,6 +117,15 @@ function useSearchChange(options = {}) {
101
117
  const resetKeys = options.resetKeys ?? RESET_KEYS;
102
118
  const joined = resetKeys.join("\x00");
103
119
  return useCallback2((changes) => {
120
+ const navigation = { replace: true, preventScrollReset: true };
121
+ if (typeof changes === "function") {
122
+ setSearchParams((current) => changes(new URLSearchParams(current)), navigation);
123
+ return;
124
+ }
125
+ if (changes instanceof URLSearchParams) {
126
+ setSearchParams(new URLSearchParams(changes), navigation);
127
+ return;
128
+ }
104
129
  setSearchParams((current) => {
105
130
  const next = new URLSearchParams(current);
106
131
  for (const [key, value] of Object.entries(changes)) {
@@ -112,15 +137,18 @@ function useSearchChange(options = {}) {
112
137
  for (const key of joined === "" ? [] : joined.split("\x00"))
113
138
  next.delete(key);
114
139
  return next;
115
- }, { replace: true, preventScrollReset: true });
140
+ }, navigation);
116
141
  }, [setSearchParams, joined]);
117
142
  }
118
143
  export {
119
144
  DEFAULT_CURSOR_PARAM,
145
+ FAILED,
146
+ isFailedPage,
147
+ orFailed,
120
148
  usePagedRows,
121
149
  useSameRouteLoading,
122
150
  useSearchChange
123
151
  };
124
152
 
125
- //# debugId=D936887B843025CD64756E2164756E21
153
+ //# debugId=CCC4EB19C4BC264164756E2164756E21
126
154
  //# sourceMappingURL=index.js.map
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lists/use-paged-rows.ts", "../src/lists/use-same-route-loading.ts", "../src/lists/use-search-change.ts"],
3
+ "sources": ["../src/lists/or-failed.ts", "../src/lists/use-paged-rows.ts", "../src/lists/use-same-route-loading.ts", "../src/lists/use-search-change.ts"],
4
4
  "sourcesContent": [
5
+ "/** What a loader answers for a failed cursor page: no rows, so `usePagedRows` sets `error`. */\nexport const FAILED = Object.freeze({ failed: true as const });\n\n/** The type of {@link FAILED}. */\nexport type FailedPage = typeof FAILED;\n\n/** Whether a loader answer is {@link FAILED}. */\nexport function isFailedPage(value: unknown): value is FailedPage {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t(value as { failed?: unknown }).failed === true\n\t);\n}\n\n/**\n * The loader half of `usePagedRows`. Runs `run` and returns its answer.\n *\n * - With no `after` (the first page), an error is rethrown, so the route's\n * `ErrorBoundary` shows.\n * - With an `after` (a \"load more\"), any error becomes {@link FAILED}, so the\n * rows stay on screen with a retry, except a 3xx `Response` (a redirect),\n * which is rethrown.\n */\nexport async function orFailed<T>(\n\tafter: string | null | undefined,\n\trun: () => Promise<T> | T,\n): Promise<T | FailedPage> {\n\tif (!after) return await run();\n\ttry {\n\t\treturn await run();\n\t} catch (error) {\n\t\tif (error instanceof Response && error.status >= 300 && error.status < 400)\n\t\t\tthrow error;\n\t\treturn FAILED;\n\t}\n}\n",
5
6
  "import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useFetcher, useLocation } from 'react-router';\n\nexport interface PagedRowsOptions<Page> {\n\t/** The search parameter that carries the cursor (default `after`). */\n\treadonly cursorParam?: string;\n\t/** The cursor of the page after this one, `null` at the end (default `page.next`). */\n\treadonly nextOf?: (page: Page) => string | null;\n}\n\nexport interface PagedRows<Row> {\n\t/** The first page's rows, then the rows of each page loaded after it. */\n\treadonly rows: readonly Row[];\n\t/** The cursor of the next page, `null` when there is none. */\n\treadonly next: string | null;\n\t/** A page is being fetched. */\n\treadonly loading: boolean;\n\t/**\n\t * The last `loadMore` ended with nothing usable (the route answered something\n\t * that is not a page). The rows stay; call `loadMore` again to retry. The\n\t * next `loadMore` clears it.\n\t */\n\treadonly error: boolean;\n\t/** Fetches the page after the last one; nothing when there is none or one is on its way. */\n\treadonly loadMore: () => void;\n}\n\n/** The default cursor parameter, shared with `useSearchChange`'s default `resetKeys`. */\nexport const DEFAULT_CURSOR_PARAM = 'after';\n\ninterface Loaded<Page, Row> {\n\treadonly first: Page;\n\treadonly extra: readonly Row[];\n\treadonly next: string | null;\n\treadonly error: boolean;\n}\n\ntype NextOf<Page> = (page: Page) => string | null;\n\n/** The state for a first page: nothing loaded after it yet. */\nfunction restart<Page, Row>(\n\tfirst: Page,\n\tnextOf: NextOf<Page>,\n): Loaded<Page, Row> {\n\treturn { first, extra: [], next: nextOf(first) ?? null, error: false };\n}\n\n/**\n * The rows and cursor of `value` when it is a page (an object whose rows are\n * an array and whose cursor is a string or `null`), else `undefined`.\n */\nfunction readPage<Page, Row>(\n\tvalue: unknown,\n\trowsOf: (page: Page) => readonly Row[],\n\tnextOf: NextOf<Page>,\n): { rows: readonly Row[]; next: string | null } | undefined {\n\tif (typeof value !== 'object' || value === null) return undefined;\n\ttry {\n\t\tconst rows = rowsOf(value as Page);\n\t\tconst next = nextOf(value as Page);\n\t\tif (!Array.isArray(rows)) return undefined;\n\t\tif (next !== null && typeof next !== 'string') return undefined;\n\t\treturn { rows, next };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/** The state after an answer: its rows appended, or `error` when it was not a page. */\nfunction settle<Page, Row>(\n\tprevious: Loaded<Page, Row>,\n\tfirst: Page,\n\tmore: { rows: readonly Row[]; next: string | null } | undefined,\n\tnextOf: NextOf<Page>,\n): Loaded<Page, Row> {\n\tconst base =\n\t\tprevious.first === first ? previous : restart<Page, Row>(first, nextOf);\n\tif (more === undefined) return { ...base, error: true };\n\treturn {\n\t\t...base,\n\t\textra: [...base.extra, ...more.rows],\n\t\tnext: more.next,\n\t\terror: false,\n\t};\n}\n\n/**\n * Appends the pages a \"load more\" asks of the route the page is on. The\n * route's loader answers `?<cursorParam>=<next>` with the next page, through\n * `useFetcher`, so the address bar and the page's own data stay as they are.\n *\n * - It starts over whenever `firstPage` is another object (a new search, a\n * revalidation): the rows are the new first page's alone, in that very\n * render. A page still on its way for the old one is dropped.\n * - A failed load keeps the rows and sets `error`. A thrown loader error goes\n * to the route's `ErrorBoundary` as React Router always does; `error` covers\n * an answer that is not a page, such as `data({ message }, { status: 503 })`\n * or `null`.\n * - Data of the fetcher that is not a page (no rows, or no cursor) is ignored.\n */\nexport function usePagedRows<Page, Row>(\n\tfirstPage: Page,\n\trowsOf: (page: Page) => readonly Row[],\n\toptions: PagedRowsOptions<Page> = {},\n): PagedRows<Row> {\n\tconst cursorParam = options.cursorParam ?? DEFAULT_CURSOR_PARAM;\n\tconst nextOf: NextOf<Page> =\n\t\toptions.nextOf ??\n\t\t((page) => (page as { next?: string | null }).next as string | null);\n\tconst fetcher = useFetcher();\n\tconst { pathname, search } = useLocation();\n\tconst [loaded, setLoaded] = useState(() =>\n\t\trestart<Page, Row>(firstPage, nextOf),\n\t);\n\t// What was asked, and for which first page, so an answer that comes after a restart is dropped.\n\tconst asked = useRef<{ first: Page; seen: unknown; started: boolean } | null>(\n\t\tnull,\n\t);\n\tconst current =\n\t\tloaded.first === firstPage ? loaded : restart<Page, Row>(firstPage, nextOf);\n\n\t// Deliberate deps: rowsOf and nextOf are usually inline functions, new on\n\t// every render, and are read at the time of the answer; the effect must run\n\t// on the fetcher's settling, not on each render.\n\tuseEffect(() => {\n\t\tconst request = asked.current;\n\t\tif (request === null) return;\n\t\tif (fetcher.state !== 'idle') {\n\t\t\trequest.started = true;\n\t\t\treturn;\n\t\t}\n\t\t// Idle: the answer is in when the fetcher was seen busy, or when its data\n\t\t// is no longer the one it had at the call (an answer within one batch).\n\t\tif (!request.started && fetcher.data === request.seen) return;\n\t\tasked.current = null;\n\t\tif (request.first !== firstPage) return;\n\t\tconst more =\n\t\t\tfetcher.data === request.seen\n\t\t\t\t? undefined\n\t\t\t\t: readPage(fetcher.data, rowsOf, nextOf);\n\t\tsetLoaded((previous) => settle(previous, firstPage, more, nextOf));\n\t}, [fetcher.state, fetcher.data, firstPage]);\n\n\tconst next = current.next;\n\tconst busy = fetcher.state !== 'idle';\n\tconst { load } = fetcher;\n\tconst loadMore = useCallback(() => {\n\t\tif (next === null || busy) return;\n\t\tconst params = new URLSearchParams(search);\n\t\tparams.set(cursorParam, next);\n\t\tasked.current = { first: firstPage, seen: fetcher.data, started: false };\n\t\tsetLoaded({ ...current, error: false });\n\t\tvoid load(`${pathname}?${params}`);\n\t}, [\n\t\tnext,\n\t\tbusy,\n\t\tsearch,\n\t\tcursorParam,\n\t\tfirstPage,\n\t\tfetcher.data,\n\t\tcurrent,\n\t\tpathname,\n\t\tload,\n\t]);\n\n\treturn {\n\t\trows: [...rowsOf(firstPage), ...current.extra],\n\t\tnext,\n\t\tloading: busy,\n\t\terror: current.error,\n\t\tloadMore,\n\t};\n}\n",
6
7
  "import { useLocation, useNavigation } from 'react-router';\n\n/**\n * `true` while a navigation is loading and goes to the pathname the page is\n * on: another search of this list, not another page. It stays `false` for a\n * navigation elsewhere (and when idle), so a list can show its skeleton\n * without flashing on a click that leaves it.\n */\nexport function useSameRouteLoading(): boolean {\n\tconst navigation = useNavigation();\n\tconst { pathname } = useLocation();\n\treturn (\n\t\tnavigation.state === 'loading' && navigation.location.pathname === pathname\n\t);\n}\n",
7
- "import { useCallback } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { DEFAULT_CURSOR_PARAM } from './use-paged-rows';\n\nexport interface SearchChangeOptions {\n\t/**\n\t * The parameters dropped by every change, because they only mean something\n\t * for the search they were read from (default `['after']`, a cursor).\n\t */\n\treadonly resetKeys?: readonly string[];\n}\n\nconst RESET_KEYS: readonly string[] = [DEFAULT_CURSOR_PARAM];\n\n/**\n * Returns `(changes) => void`: sets the parameters it is given a value for,\n * deletes the ones given `null` or `undefined` (an empty string is a value,\n * so it is set), and deletes `resetKeys` in\n * the same step. It replaces the history entry and keeps the scroll position.\n */\nexport function useSearchChange(\n\toptions: SearchChangeOptions = {},\n): (changes: Record<string, string | null | undefined>) => void {\n\tconst [, setSearchParams] = useSearchParams();\n\tconst resetKeys = options.resetKeys ?? RESET_KEYS;\n\tconst joined = resetKeys.join('\\u0000');\n\treturn useCallback(\n\t\t(changes) => {\n\t\t\tsetSearchParams(\n\t\t\t\t(current) => {\n\t\t\t\t\tconst next = new URLSearchParams(current);\n\t\t\t\t\tfor (const [key, value] of Object.entries(changes)) {\n\t\t\t\t\t\tif (value === null || value === undefined) next.delete(key);\n\t\t\t\t\t\telse next.set(key, value);\n\t\t\t\t\t}\n\t\t\t\t\tfor (const key of joined === '' ? [] : joined.split('\\u0000'))\n\t\t\t\t\t\tnext.delete(key);\n\t\t\t\t\treturn next;\n\t\t\t\t},\n\t\t\t\t{ replace: true, preventScrollReset: true },\n\t\t\t);\n\t\t},\n\t\t[setSearchParams, joined],\n\t);\n}\n"
8
+ "import { useCallback } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { DEFAULT_CURSOR_PARAM } from './use-paged-rows';\n\nexport interface SearchChangeOptions {\n\t/**\n\t * The parameters dropped by every change, because they only mean something\n\t * for the search they were read from (default `['after']`, a cursor).\n\t */\n\treadonly resetKeys?: readonly string[];\n}\n\nconst RESET_KEYS: readonly string[] = [DEFAULT_CURSOR_PARAM];\n\n/** What `change` takes: a record of changes, a whole search, or an updater. */\nexport type SearchChange =\n\t| Record<string, string | null | undefined>\n\t| URLSearchParams\n\t| ((prev: URLSearchParams) => URLSearchParams);\n\n/**\n * Returns `(changes) => void`. With a record it sets the parameters it is\n * given a value for, deletes the ones given `null` or `undefined` (an empty\n * string is a value, so it is set), and deletes `resetKeys` in the same step.\n * With a `URLSearchParams` it replaces the whole search by it, and with an\n * updater `(prev) => URLSearchParams` by what the updater returns; both are\n * the caller's whole search, so `resetKeys` does not apply. Every form\n * replaces the history entry and keeps the scroll position.\n */\nexport function useSearchChange(\n\toptions: SearchChangeOptions = {},\n): (changes: SearchChange) => void {\n\tconst [, setSearchParams] = useSearchParams();\n\tconst resetKeys = options.resetKeys ?? RESET_KEYS;\n\tconst joined = resetKeys.join('\\u0000');\n\treturn useCallback(\n\t\t(changes) => {\n\t\t\tconst navigation = { replace: true, preventScrollReset: true };\n\t\t\tif (typeof changes === 'function') {\n\t\t\t\tsetSearchParams(\n\t\t\t\t\t(current) => changes(new URLSearchParams(current)),\n\t\t\t\t\tnavigation,\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (changes instanceof URLSearchParams) {\n\t\t\t\tsetSearchParams(new URLSearchParams(changes), navigation);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsetSearchParams((current) => {\n\t\t\t\tconst next = new URLSearchParams(current);\n\t\t\t\tfor (const [key, value] of Object.entries(changes)) {\n\t\t\t\t\tif (value === null || value === undefined) next.delete(key);\n\t\t\t\t\telse next.set(key, value);\n\t\t\t\t}\n\t\t\t\tfor (const key of joined === '' ? [] : joined.split('\\u0000'))\n\t\t\t\t\tnext.delete(key);\n\t\t\t\treturn next;\n\t\t\t}, navigation);\n\t\t},\n\t\t[setSearchParams, joined],\n\t);\n}\n"
8
9
  ],
9
- "mappings": "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",
10
- "debugId": "D936887B843025CD64756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "CCC4EB19C4BC264164756E2164756E21",
11
12
  "names": []
12
13
  }
@@ -0,0 +1,19 @@
1
+ /** What a loader answers for a failed cursor page: no rows, so `usePagedRows` sets `error`. */
2
+ export declare const FAILED: Readonly<{
3
+ failed: true;
4
+ }>;
5
+ /** The type of {@link FAILED}. */
6
+ export type FailedPage = typeof FAILED;
7
+ /** Whether a loader answer is {@link FAILED}. */
8
+ export declare function isFailedPage(value: unknown): value is FailedPage;
9
+ /**
10
+ * The loader half of `usePagedRows`. Runs `run` and returns its answer.
11
+ *
12
+ * - With no `after` (the first page), an error is rethrown, so the route's
13
+ * `ErrorBoundary` shows.
14
+ * - With an `after` (a "load more"), any error becomes {@link FAILED}, so the
15
+ * rows stay on screen with a retry, except a 3xx `Response` (a redirect),
16
+ * which is rethrown.
17
+ */
18
+ export declare function orFailed<T>(after: string | null | undefined, run: () => Promise<T> | T): Promise<T | FailedPage>;
19
+ //# sourceMappingURL=or-failed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"or-failed.d.ts","sourceRoot":"","sources":["../../src/lists/or-failed.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,eAAO,MAAM,MAAM;;EAA2C,CAAC;AAE/D,kCAAkC;AAClC,MAAM,MAAM,UAAU,GAAG,OAAO,MAAM,CAAC;AAEvC,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE;AAED;;;;;;;;GAQG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAC/B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GACvB,OAAO,CAAC,CAAC,GAAG,UAAU,CAAC,CASzB"}
@@ -5,11 +5,16 @@ export interface SearchChangeOptions {
5
5
  */
6
6
  readonly resetKeys?: readonly string[];
7
7
  }
8
+ /** What `change` takes: a record of changes, a whole search, or an updater. */
9
+ export type SearchChange = Record<string, string | null | undefined> | URLSearchParams | ((prev: URLSearchParams) => URLSearchParams);
8
10
  /**
9
- * Returns `(changes) => void`: sets the parameters it is given a value for,
10
- * deletes the ones given `null` or `undefined` (an empty string is a value,
11
- * so it is set), and deletes `resetKeys` in
12
- * the same step. It replaces the history entry and keeps the scroll position.
11
+ * Returns `(changes) => void`. With a record it sets the parameters it is
12
+ * given a value for, deletes the ones given `null` or `undefined` (an empty
13
+ * string is a value, so it is set), and deletes `resetKeys` in the same step.
14
+ * With a `URLSearchParams` it replaces the whole search by it, and with an
15
+ * updater `(prev) => URLSearchParams` by what the updater returns; both are
16
+ * the caller's whole search, so `resetKeys` does not apply. Every form
17
+ * replaces the history entry and keeps the scroll position.
13
18
  */
14
- export declare function useSearchChange(options?: SearchChangeOptions): (changes: Record<string, string | null | undefined>) => void;
19
+ export declare function useSearchChange(options?: SearchChangeOptions): (changes: SearchChange) => void;
15
20
  //# sourceMappingURL=use-search-change.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-search-change.d.ts","sourceRoot":"","sources":["../../src/lists/use-search-change.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IACnC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,OAAO,GAAE,mBAAwB,GAC/B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,KAAK,IAAI,CAsB9D"}
1
+ {"version":3,"file":"use-search-change.d.ts","sourceRoot":"","sources":["../../src/lists/use-search-change.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IACnC;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAID,+EAA+E;AAC/E,MAAM,MAAM,YAAY,GACrB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GACzC,eAAe,GACf,CAAC,CAAC,IAAI,EAAE,eAAe,KAAK,eAAe,CAAC,CAAC;AAEhD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC9B,OAAO,GAAE,mBAAwB,GAC/B,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CA+BjC"}
@@ -62,7 +62,8 @@ export function codeOf(error: unknown) {
62
62
  ```
63
63
 
64
64
  An `ApiError` is assignable to `ApiErrorLike` as it is. The shape's code is
65
- the API's own `<module>.errors.<slug>`, such as `todos.errors.title-taken`.
65
+ the API's own `<module>.errors.<slug>`, such as `todos.errors.title-taken`
66
+ or `userProfiles.errors.slug-taken` (the module is kebab-case or camelCase).
66
67
 
67
68
  `waitMinutes` reads a throttle's wait the way the API words it: the
68
69
  `minutes` of `extensions.params` or beside it, at least one, else the
@@ -261,7 +262,8 @@ it once, at module scope: a new function re-creates the resolver.
261
262
  Two options adapt it. `codeOf(issue)` says which code an issue carries (the
262
263
  refinement's `params.code` by default); an app that also words an empty field
263
264
  as the API's `validation.errors.required` passes its own. `isApiCode(key)`
264
- says which keys are API codes (`<module>.errors.<slug>` by default).
265
+ says which keys are API codes (`<module>.errors.<slug>` by default, the
266
+ module in kebab-case or camelCase).
265
267
 
266
268
  ```ts
267
269
  import { apiCodeResolver } from '@alxia/react-router/errors';
@@ -397,8 +399,9 @@ code), `translateCode`'s, nor the entries of `extensions.fields`, which go
397
399
  straight to the catalogue.
398
400
 
399
401
  `isApiCode` changes what counts as an API code (`<module>.errors.<slug>` by
400
- default). Here only the codes of the `todos` module are the catalogue's; any
401
- other code, even one shaped like a catalogue code, says `refused`:
402
+ default, the module in kebab-case or camelCase). Here only the codes of the
403
+ `todos` module are the catalogue's; any other code, even one shaped like a
404
+ catalogue code, says `refused`:
402
405
 
403
406
  ```ts
404
407
  export const apiErrors = createApiErrors<'en' | 'fr'>({
@@ -47,6 +47,18 @@ meaningless. It navigates with `replace: true` and `preventScrollReset: true`,
47
47
  so typing does not fill the history nor jump the page. Pass
48
48
  `{ resetKeys: ['after', 'page'] }` to drop more.
49
49
 
50
+ `change` also takes a whole `URLSearchParams`, which replaces the search, or an
51
+ updater `(prev) => URLSearchParams`, for a filter bar or a server table that
52
+ hands back its next parameters. Both navigate exactly as the record form does
53
+ (`replace: true`, `preventScrollReset: true`), and since they are the whole
54
+ search, `resetKeys` does not apply to them: drop the cursor yourself if the
55
+ new search needs it gone.
56
+
57
+ ```tsx
58
+ <Table onParamsChange={(next: URLSearchParams) => change(next)} />;
59
+ change((prev) => new URLSearchParams([...prev].filter(([key]) => key !== 'status')));
60
+ ```
61
+
50
62
  ## `useSameRouteLoading`
51
63
 
52
64
  ```tsx
@@ -107,5 +119,22 @@ with another one a search that keeps the old cursor shows the wrong page.
107
119
 
108
120
  A loader that **throws** on a cursor request is not an `error` here: React
109
121
  Router sends a fetcher's thrown error to the route's `ErrorBoundary`. To offer
110
- a retry instead, catch in the loader for a request that has the cursor and
111
- answer a value that is not a page.
122
+ a retry instead, wrap the read in `orFailed(after, run)`: it answers
123
+ `FAILED` (`{ failed: true }`, a value that is not a page) for a request that
124
+ has the cursor, rethrows when there is none and rethrows a 3xx `Response`.
125
+
126
+ ```ts
127
+ import { orFailed } from '@alxia/react-router/lists';
128
+
129
+ export function loader({ request }: Route.LoaderArgs) {
130
+ const url = new URL(request.url);
131
+ return orFailed(url.searchParams.get('after'), async () => ({
132
+ page: await listUsers({
133
+ q: url.searchParams.get('q') ?? '',
134
+ after: url.searchParams.get('after'),
135
+ }),
136
+ }));
137
+ }
138
+ ```
139
+
140
+ `isFailedPage(loaderData)` narrows the answer when a component reads it.
@@ -539,7 +539,16 @@ name.
539
539
 
540
540
  **Why:** React Router sends the error of a fetcher's load to the route's boundary. `usePagedRows` only sees what the fetcher answers, so its `error` is `true` for an answer that is not a page, not for a throw.
541
541
 
542
- **Fix:** catch in the loader for a request that has the cursor and answer a value that is not a page:
542
+ **Fix:** wrap the read in `orFailed(after, run)` from `@alxia/react-router/lists`, which answers `{ failed: true }` for a request that has the cursor and rethrows otherwise (and rethrows a redirect):
543
+
544
+ ```ts
545
+ export function loader({ request }: Route.LoaderArgs) {
546
+ const url = new URL(request.url);
547
+ return orFailed(url.searchParams.get('after'), () => listUsers(url.searchParams));
548
+ }
549
+ ```
550
+
551
+ By hand, that is:
543
552
 
544
553
  ```ts
545
554
  export async function loader({ request }: Route.LoaderArgs) {
@@ -2419,6 +2428,9 @@ failed before the API's validation layer), or a code that `isApiCode`
2419
2428
  rejects. Without `refused`, `createApiErrors` says the API's own message for
2420
2429
  such an error.
2421
2430
 
2431
+ Versions before the camelCase patch also rejected a camelCase module
2432
+ (`userProfiles.errors.slug-taken`): upgrade, or pass `isApiCode`.
2433
+
2422
2434
  **Fix:** pass `refused`, the app's generic refusal; it replaces the raw
2423
2435
  message for every error that is not an API code
2424
2436
  ([wording a refusal](guide/errors.md#wording-a-refusal)):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/react-router",
3
- "version": "0.23.0",
3
+ "version": "0.24.1",
4
4
  "description": "alxia as the server of a React Router framework app: server rendering behind the app's hooks, loaders reading its typed context, the client's assets served; the code with no UI an installable app shares (`/pwa`), its hooks (`/pwa/react`), `noCache` for the worker's script (`/pwa/server`), the service worker on Workbox (`/pwa/worker`) and its build (`/pwa/build`), an API's refusals worded for the page (`/errors`), the BFF session: cookies, sign-in, step-up and mail links (`/session`), its page hooks (`/session/react`) and what it shares with the page (`/session/model`), a GraphQL subscription relayed to the browser over SSE (`/subscription`), the page's CSP nonce and inline boot scripts (`/document`) and a streaming `entry.server` (`/entry`)",
5
5
  "license": "MIT",
6
6
  "type": "module",