@entifix/react-integration 0.1.0-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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/context/adapters-context/adapters-context.d.ts +4 -0
- package/dist/context/adapters-context/adapters-context.d.ts.map +1 -0
- package/dist/context/adapters-context/adapters-context.js +19 -0
- package/dist/context/adapters-context/adapters-context.js.map +1 -0
- package/dist/context/adapters-context/index.d.ts +2 -0
- package/dist/context/adapters-context/index.d.ts.map +1 -0
- package/dist/context/adapters-context/index.js +3 -0
- package/dist/context/adapters-context/index.js.map +1 -0
- package/dist/hooks/use-data-loading/index.d.ts +3 -0
- package/dist/hooks/use-data-loading/index.d.ts.map +1 -0
- package/dist/hooks/use-data-loading/index.js +4 -0
- package/dist/hooks/use-data-loading/index.js.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.d.ts +26 -0
- package/dist/hooks/use-data-loading/use-data-loading.d.ts.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.js +167 -0
- package/dist/hooks/use-data-loading/use-data-loading.js.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.d.ts +53 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.d.ts.map +1 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.js +3 -0
- package/dist/hooks/use-data-loading/use-data-loading.types.js.map +1 -0
- package/dist/hooks/use-entity-form/index.d.ts +4 -0
- package/dist/hooks/use-entity-form/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/index.js +5 -0
- package/dist/hooks/use-entity-form/index.js.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.d.ts +31 -0
- package/dist/hooks/use-entity-form/use-entity-form.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts +136 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.js +337 -0
- package/dist/hooks/use-entity-form/use-entity-form.helpers.js.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.js +209 -0
- package/dist/hooks/use-entity-form/use-entity-form.js.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.d.ts +173 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.js +3 -0
- package/dist/hooks/use-entity-form/use-entity-form.types.js.map +1 -0
- package/dist/hooks/use-entity-link-resolver/index.d.ts +2 -0
- package/dist/hooks/use-entity-link-resolver/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-resolver/index.js +3 -0
- package/dist/hooks/use-entity-link-resolver/index.js.map +1 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts +17 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js +26 -0
- package/dist/hooks/use-entity-link-resolver/use-entity-link-resolver.js.map +1 -0
- package/dist/hooks/use-entity-link-source/index.d.ts +3 -0
- package/dist/hooks/use-entity-link-source/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-source/index.js +4 -0
- package/dist/hooks/use-entity-link-source/index.js.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts +25 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.js +169 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.js.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts +49 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js +3 -0
- package/dist/hooks/use-entity-link-source/use-entity-link-source.types.js.map +1 -0
- package/dist/hooks/use-entity-mutation/index.d.ts +3 -0
- package/dist/hooks/use-entity-mutation/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-mutation/index.js +4 -0
- package/dist/hooks/use-entity-mutation/index.js.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts +22 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.d.ts.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.js +88 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.js.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts +19 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js +3 -0
- package/dist/hooks/use-entity-mutation/use-entity-mutation.types.js.map +1 -0
- package/dist/hooks/use-entity-record/index.d.ts +3 -0
- package/dist/hooks/use-entity-record/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-record/index.js +4 -0
- package/dist/hooks/use-entity-record/index.js.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.d.ts +21 -0
- package/dist/hooks/use-entity-record/use-entity-record.d.ts.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.js +93 -0
- package/dist/hooks/use-entity-record/use-entity-record.js.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.d.ts +22 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.d.ts.map +1 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.js +3 -0
- package/dist/hooks/use-entity-record/use-entity-record.types.js.map +1 -0
- package/dist/hooks/use-entity-use-cases/index.d.ts +2 -0
- package/dist/hooks/use-entity-use-cases/index.d.ts.map +1 -0
- package/dist/hooks/use-entity-use-cases/index.js +3 -0
- package/dist/hooks/use-entity-use-cases/index.js.map +1 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts +25 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.d.ts.map +1 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js +47 -0
- package/dist/hooks/use-entity-use-cases/use-entity-use-cases.js.map +1 -0
- package/dist/hooks/use-wizard/index.d.ts +3 -0
- package/dist/hooks/use-wizard/index.d.ts.map +1 -0
- package/dist/hooks/use-wizard/index.js +4 -0
- package/dist/hooks/use-wizard/index.js.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.d.ts +19 -0
- package/dist/hooks/use-wizard/use-wizard.d.ts.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.js +136 -0
- package/dist/hooks/use-wizard/use-wizard.js.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.types.d.ts +87 -0
- package/dist/hooks/use-wizard/use-wizard.types.d.ts.map +1 -0
- package/dist/hooks/use-wizard/use-wizard.types.js +3 -0
- package/dist/hooks/use-wizard/use-wizard.types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/query/combine-filtering.d.ts +11 -0
- package/dist/query/combine-filtering.d.ts.map +1 -0
- package/dist/query/combine-filtering.js +18 -0
- package/dist/query/combine-filtering.js.map +1 -0
- package/dist/query/entity-query-key.d.ts +28 -0
- package/dist/query/entity-query-key.d.ts.map +1 -0
- package/dist/query/entity-query-key.js +28 -0
- package/dist/query/entity-query-key.js.map +1 -0
- package/dist/query/query-provider.d.ts +21 -0
- package/dist/query/query-provider.d.ts.map +1 -0
- package/dist/query/query-provider.js +35 -0
- package/dist/query/query-provider.js.map +1 -0
- package/dist/reactive/event-source-channel.d.ts +22 -0
- package/dist/reactive/event-source-channel.d.ts.map +1 -0
- package/dist/reactive/event-source-channel.js +93 -0
- package/dist/reactive/event-source-channel.js.map +1 -0
- package/dist/reactive/reactive-channel.d.ts +75 -0
- package/dist/reactive/reactive-channel.d.ts.map +1 -0
- package/dist/reactive/reactive-channel.js +55 -0
- package/dist/reactive/reactive-channel.js.map +1 -0
- package/dist/reactive/use-reactive-invalidation.d.ts +10 -0
- package/dist/reactive/use-reactive-invalidation.d.ts.map +1 -0
- package/dist/reactive/use-reactive-invalidation.js +25 -0
- package/dist/reactive/use-reactive-invalidation.js.map +1 -0
- package/dist/reactive/use-transaction-settlement.d.ts +41 -0
- package/dist/reactive/use-transaction-settlement.d.ts.map +1 -0
- package/dist/reactive/use-transaction-settlement.js +123 -0
- package/dist/reactive/use-transaction-settlement.js.map +1 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 r10c Technologies
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters-context.d.ts","sourceRoot":"","sources":["../../../src/context/adapters-context/adapters-context.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,OAAO,EAA6B,MAAM,OAAO,CAAC;AAEhE,wBAAgB,qBAAqB,CAAC,SAAS,wBAE9C;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,aAaxE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { EntifixBuildError } from '@entifix/core';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
export function createAdaptersContext() {
|
|
4
|
+
return /*#__PURE__*/ createContext({});
|
|
5
|
+
}
|
|
6
|
+
export function useAdaptersContext(context) {
|
|
7
|
+
const contextValue = useContext(context);
|
|
8
|
+
// The default `createAdaptersContext` hands out is `{}`, so "nobody mounted
|
|
9
|
+
// a provider" and "the object has no keys" are the same question. Asked
|
|
10
|
+
// directly rather than through a generic `isEmpty`: that helper also answers
|
|
11
|
+
// for strings, arrays and null, none of which can reach here, and it was the
|
|
12
|
+
// single import crossing out of entifix.
|
|
13
|
+
if (Object.keys(contextValue).length === 0) {
|
|
14
|
+
throw new EntifixBuildError('Adapters context is empty. Make sure to wrap your component tree with the appropriate AdaptersProvider.');
|
|
15
|
+
}
|
|
16
|
+
return contextValue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=adapters-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/context/adapters-context/adapters-context.tsx"],"sourcesContent":["import { EntifixBuildError } from '@entifix/core';\nimport { type Context, createContext, useContext } from 'react';\n\nexport function createAdaptersContext<TAdapters>() {\n return createContext({} as TAdapters);\n}\n\nexport function useAdaptersContext<TAdapters>(context: Context<TAdapters>) {\n const contextValue = useContext(context);\n // The default `createAdaptersContext` hands out is `{}`, so \"nobody mounted\n // a provider\" and \"the object has no keys\" are the same question. Asked\n // directly rather than through a generic `isEmpty`: that helper also answers\n // for strings, arrays and null, none of which can reach here, and it was the\n // single import crossing out of entifix.\n if (Object.keys(contextValue as object).length === 0) {\n throw new EntifixBuildError(\n 'Adapters context is empty. Make sure to wrap your component tree with the appropriate AdaptersProvider.',\n );\n }\n return contextValue;\n}\n"],"names":["EntifixBuildError","createContext","useContext","createAdaptersContext","useAdaptersContext","context","contextValue","Object","keys","length"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,gBAAgB;AAClD,SAAuBC,aAAa,EAAEC,UAAU,QAAQ,QAAQ;AAEhE,OAAO,SAASC;IACd,qBAAOF,cAAc,CAAC;AACxB;AAEA,OAAO,SAASG,mBAA8BC,OAA2B;IACvE,MAAMC,eAAeJ,WAAWG;IAChC,4EAA4E;IAC5E,wEAAwE;IACxE,6EAA6E;IAC7E,6EAA6E;IAC7E,yCAAyC;IACzC,IAAIE,OAAOC,IAAI,CAACF,cAAwBG,MAAM,KAAK,GAAG;QACpD,MAAM,IAAIT,kBACR;IAEJ;IACA,OAAOM;AACT"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/context/adapters-context/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/context/adapters-context/index.ts"],"sourcesContent":["export * from './adapters-context.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/use-data-loading/index.ts"],"sourcesContent":["export * from './use-data-loading.js';\nexport * from './use-data-loading.types.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB;AACtC,cAAc,8BAA8B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type EntifixError, type Entity, type EntitySorting, type FilterGroup } from '@entifix/core';
|
|
2
|
+
import type { UseDataLoadingOptions } from './use-data-loading.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Runs an entity list use-case and caches the result through TanStack Query.
|
|
5
|
+
*
|
|
6
|
+
* The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the
|
|
7
|
+
* *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations
|
|
8
|
+
* and reactive events invalidate the list. The public shape is unchanged: the
|
|
9
|
+
* `uc`/`ctx` come in the same way and the Effect runs against the same provided
|
|
10
|
+
* Tags, so callers and organisms are untouched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useDataLoading<TEntity extends Entity, TContext>({ uc, ctx, initialPageSize, queryKey, enabled, baseFiltering, }: UseDataLoadingOptions<TEntity, TContext>): {
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
items: TEntity[];
|
|
15
|
+
totalItems: number;
|
|
16
|
+
currentPage: number;
|
|
17
|
+
pageSize: number;
|
|
18
|
+
error: EntifixError | undefined;
|
|
19
|
+
filtering: FilterGroup<TEntity> | undefined;
|
|
20
|
+
sorting: EntitySorting<TEntity> | undefined;
|
|
21
|
+
onPageChange: (newPage: number) => void;
|
|
22
|
+
onPageSizeChange: (newPageSize: number) => void;
|
|
23
|
+
onFilteringChange: (filtering: FilterGroup<TEntity>) => void;
|
|
24
|
+
onSortingChange: (sorting: EntitySorting<TEntity>) => void;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=use-data-loading.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-data-loading.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/use-data-loading.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,MAAM,EAGX,KAAK,aAAa,EAClB,KAAK,WAAW,EAGjB,MAAM,eAAe,CAAC;AAavB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAiCzE;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ,EAAE,EAC/D,EAAE,EACF,GAAG,EACH,eAAmC,EACnC,QAAQ,EACR,OAAc,EACd,aAAa,GACd,EAAE,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC;;;;;;;;;4BA0GE,MAAM;oCAIE,MAAM;mCAMP,WAAW,CAAC,OAAO,CAAC;+BAIxB,aAAa,CAAC,OAAO,CAAC;EAkBrE"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { EntityLoadRequestTag } from '@entifix/business';
|
|
3
|
+
import { serializeRsql, serializeSort } from '@entifix/core';
|
|
4
|
+
import { keepPreviousData, useQuery } from '@tanstack/react-query';
|
|
5
|
+
import { Context, Effect } from 'effect';
|
|
6
|
+
import { useCallback, useEffect, useId, useMemo, useReducer, useRef } from 'react';
|
|
7
|
+
import { combineFilterGroups } from '../../query/combine-filtering.js';
|
|
8
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
9
|
+
function requestReducer(state, action) {
|
|
10
|
+
return _extends({}, state, action.set);
|
|
11
|
+
}
|
|
12
|
+
function initialRequest(pageSize) {
|
|
13
|
+
return {
|
|
14
|
+
currentPage: 1,
|
|
15
|
+
pageSize,
|
|
16
|
+
filtering: undefined,
|
|
17
|
+
sorting: undefined
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs an entity list use-case and caches the result through TanStack Query.
|
|
22
|
+
*
|
|
23
|
+
* The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the
|
|
24
|
+
* *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations
|
|
25
|
+
* and reactive events invalidate the list. The public shape is unchanged: the
|
|
26
|
+
* `uc`/`ctx` come in the same way and the Effect runs against the same provided
|
|
27
|
+
* Tags, so callers and organisms are untouched.
|
|
28
|
+
*/ export function useDataLoading({ uc, ctx, initialPageSize = DEFAULT_PAGE_SIZE, queryKey, enabled = true, baseFiltering }) {
|
|
29
|
+
var _ref, _ref1, _query_error;
|
|
30
|
+
var _query_data, _query_data1;
|
|
31
|
+
const [request, dispatch] = useReducer(requestReducer, initialPageSize, initialRequest);
|
|
32
|
+
// Callers rebuild `uc`/`ctx` inline every render, so hold the latest in refs
|
|
33
|
+
// and keep them out of the query key — keying on their identity would refetch
|
|
34
|
+
// on every render. Written in an effect (refs may not be set during render);
|
|
35
|
+
// effects fire in order, so the query below always reads the latest.
|
|
36
|
+
const ucRef = useRef(uc);
|
|
37
|
+
const ctxRef = useRef(ctx);
|
|
38
|
+
useEffect(()=>{
|
|
39
|
+
ucRef.current = uc;
|
|
40
|
+
ctxRef.current = ctx;
|
|
41
|
+
});
|
|
42
|
+
// What actually goes on the wire: the caller's standing restriction ANDed with
|
|
43
|
+
// whatever the user applied. Kept separate from `request.filtering` because the
|
|
44
|
+
// restriction is not the user's filter — the panel must keep showing only what
|
|
45
|
+
// they set, while the request keeps carrying both.
|
|
46
|
+
const effectiveFiltering = useMemo(()=>combineFilterGroups(baseFiltering, request.filtering), [
|
|
47
|
+
baseFiltering,
|
|
48
|
+
request.filtering
|
|
49
|
+
]);
|
|
50
|
+
// The applied filter/sort are rebuilt each render, so their *serialized* form
|
|
51
|
+
// — the same codec the request needs anyway — is what keys the query: a stable
|
|
52
|
+
// string that changes exactly when the query does.
|
|
53
|
+
const rsql = useMemo(()=>serializeRsql(effectiveFiltering ? [
|
|
54
|
+
effectiveFiltering
|
|
55
|
+
] : undefined), [
|
|
56
|
+
effectiveFiltering
|
|
57
|
+
]);
|
|
58
|
+
const sort = useMemo(()=>serializeSort(request.sorting ? [
|
|
59
|
+
request.sorting
|
|
60
|
+
] : undefined), [
|
|
61
|
+
request.sorting
|
|
62
|
+
]);
|
|
63
|
+
const loadRequest = useMemo(()=>{
|
|
64
|
+
const built = {
|
|
65
|
+
page: request.currentPage,
|
|
66
|
+
pageSize: request.pageSize
|
|
67
|
+
};
|
|
68
|
+
// An empty group serializes to nothing; omit it rather than send a filter
|
|
69
|
+
// that matches everything.
|
|
70
|
+
if (rsql !== '' && effectiveFiltering) {
|
|
71
|
+
built.filtering = [
|
|
72
|
+
effectiveFiltering
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
if (sort !== '' && request.sorting) {
|
|
76
|
+
built.sorting = [
|
|
77
|
+
request.sorting
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
return built;
|
|
81
|
+
}, [
|
|
82
|
+
request.currentPage,
|
|
83
|
+
request.pageSize,
|
|
84
|
+
effectiveFiltering,
|
|
85
|
+
request.sorting,
|
|
86
|
+
rsql,
|
|
87
|
+
sort
|
|
88
|
+
]);
|
|
89
|
+
// Falls back to a per-instance id so an un-scoped list never collides with
|
|
90
|
+
// another in the shared cache — correct, just unshared (see the type doc).
|
|
91
|
+
const instanceId = useId();
|
|
92
|
+
const scope = queryKey != null ? queryKey : [
|
|
93
|
+
instanceId
|
|
94
|
+
];
|
|
95
|
+
const query = useQuery({
|
|
96
|
+
enabled,
|
|
97
|
+
/**
|
|
98
|
+
* Page, size, filter and sort are all part of the key, so every one of them
|
|
99
|
+
* is a *different* query — and without this, `data` goes undefined the
|
|
100
|
+
* instant any of them changes. The table would then see `items.length === 0`
|
|
101
|
+
* with `isFetching` true, read it as a first load, and blank populated rows
|
|
102
|
+
* to skeleton on every pagination click: the grey flash the loading contract
|
|
103
|
+
* exists to prevent. Keeping the previous page is what makes "skeleton only
|
|
104
|
+
* on first load" true rather than merely intended.
|
|
105
|
+
*/ placeholderData: keepPreviousData,
|
|
106
|
+
queryKey: [
|
|
107
|
+
...scope,
|
|
108
|
+
'load',
|
|
109
|
+
request.currentPage,
|
|
110
|
+
request.pageSize,
|
|
111
|
+
rsql,
|
|
112
|
+
sort
|
|
113
|
+
],
|
|
114
|
+
queryFn: ()=>Effect.runPromise(Effect.provide(ucRef.current, ctxRef.current.pipe(Context.add(EntityLoadRequestTag, // The tag carries no generic, so the entity-typed request crosses
|
|
115
|
+
// it — the same cast `loadUCFactory` makes reading it back.
|
|
116
|
+
loadRequest))))
|
|
117
|
+
});
|
|
118
|
+
const onPageChange = useCallback((newPage)=>{
|
|
119
|
+
dispatch({
|
|
120
|
+
set: {
|
|
121
|
+
currentPage: newPage
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}, []);
|
|
125
|
+
const onPageSizeChange = useCallback((newPageSize)=>{
|
|
126
|
+
dispatch({
|
|
127
|
+
set: {
|
|
128
|
+
pageSize: newPageSize,
|
|
129
|
+
currentPage: 1
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}, []);
|
|
133
|
+
// Narrowing the result set invalidates the page number: page 4 of the old
|
|
134
|
+
// result is very likely past the end of the new one.
|
|
135
|
+
const onFilteringChange = useCallback((filtering)=>{
|
|
136
|
+
dispatch({
|
|
137
|
+
set: {
|
|
138
|
+
filtering,
|
|
139
|
+
currentPage: 1
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}, []);
|
|
143
|
+
const onSortingChange = useCallback((sorting)=>{
|
|
144
|
+
dispatch({
|
|
145
|
+
set: {
|
|
146
|
+
sorting,
|
|
147
|
+
currentPage: 1
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}, []);
|
|
151
|
+
return {
|
|
152
|
+
isLoading: query.isFetching,
|
|
153
|
+
items: (_ref = (_query_data = query.data) == null ? void 0 : _query_data.items) != null ? _ref : [],
|
|
154
|
+
totalItems: (_ref1 = (_query_data1 = query.data) == null ? void 0 : _query_data1.total) != null ? _ref1 : 0,
|
|
155
|
+
currentPage: request.currentPage,
|
|
156
|
+
pageSize: request.pageSize,
|
|
157
|
+
error: (_query_error = query.error) != null ? _query_error : undefined,
|
|
158
|
+
filtering: request.filtering,
|
|
159
|
+
sorting: request.sorting,
|
|
160
|
+
onPageChange,
|
|
161
|
+
onPageSizeChange,
|
|
162
|
+
onFilteringChange,
|
|
163
|
+
onSortingChange
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//# sourceMappingURL=use-data-loading.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/use-data-loading/use-data-loading.ts"],"sourcesContent":["import { EntityLoadRequestTag } from '@entifix/business';\nimport {\n type EntifixError,\n type Entity,\n type EntityLoadRequest,\n type EntityPage,\n type EntitySorting,\n type FilterGroup,\n serializeRsql,\n serializeSort,\n} from '@entifix/core';\nimport { keepPreviousData, useQuery } from '@tanstack/react-query';\nimport { Context, Effect } from 'effect';\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useReducer,\n useRef,\n} from 'react';\n\nimport { combineFilterGroups } from '../../query/combine-filtering.js';\nimport type { UseDataLoadingOptions } from './use-data-loading.types.js';\n\nconst DEFAULT_PAGE_SIZE = 10;\n\n/**\n * The request the caller controls — page, size, and the *applied* filter/sort.\n * The response (items, loading, error) is owned by the query cache, not here.\n */\ninterface RequestState<T extends Entity> {\n currentPage: number;\n pageSize: number;\n filtering: FilterGroup<T> | undefined;\n sorting: EntitySorting<T> | undefined;\n}\n\ntype RequestAction<T extends Entity> = { set: Partial<RequestState<T>> };\n\nfunction requestReducer<T extends Entity>(\n state: RequestState<T>,\n action: RequestAction<T>,\n): RequestState<T> {\n return { ...state, ...action.set };\n}\n\nfunction initialRequest<T extends Entity>(pageSize: number): RequestState<T> {\n return {\n currentPage: 1,\n pageSize,\n filtering: undefined,\n sorting: undefined,\n };\n}\n\n/**\n * Runs an entity list use-case and caches the result through TanStack Query.\n *\n * The reducer owns the *request* (page/size/filter/sort); `useQuery` owns the\n * *fetch* — deduping, caching, and (with a shared `queryKey`) letting mutations\n * and reactive events invalidate the list. The public shape is unchanged: the\n * `uc`/`ctx` come in the same way and the Effect runs against the same provided\n * Tags, so callers and organisms are untouched.\n */\nexport function useDataLoading<TEntity extends Entity, TContext>({\n uc,\n ctx,\n initialPageSize = DEFAULT_PAGE_SIZE,\n queryKey,\n enabled = true,\n baseFiltering,\n}: UseDataLoadingOptions<TEntity, TContext>) {\n const [request, dispatch] = useReducer(\n requestReducer as (\n state: RequestState<TEntity>,\n action: RequestAction<TEntity>,\n ) => RequestState<TEntity>,\n initialPageSize,\n initialRequest<TEntity>,\n );\n\n // Callers rebuild `uc`/`ctx` inline every render, so hold the latest in refs\n // and keep them out of the query key — keying on their identity would refetch\n // on every render. Written in an effect (refs may not be set during render);\n // effects fire in order, so the query below always reads the latest.\n const ucRef = useRef(uc);\n const ctxRef = useRef(ctx);\n useEffect(() => {\n ucRef.current = uc;\n ctxRef.current = ctx;\n });\n\n // What actually goes on the wire: the caller's standing restriction ANDed with\n // whatever the user applied. Kept separate from `request.filtering` because the\n // restriction is not the user's filter — the panel must keep showing only what\n // they set, while the request keeps carrying both.\n const effectiveFiltering = useMemo(\n () => combineFilterGroups<TEntity>(baseFiltering, request.filtering),\n [baseFiltering, request.filtering],\n );\n\n // The applied filter/sort are rebuilt each render, so their *serialized* form\n // — the same codec the request needs anyway — is what keys the query: a stable\n // string that changes exactly when the query does.\n const rsql = useMemo(\n () => serializeRsql(effectiveFiltering ? [effectiveFiltering] : undefined),\n [effectiveFiltering],\n );\n const sort = useMemo(\n () => serializeSort(request.sorting ? [request.sorting] : undefined),\n [request.sorting],\n );\n\n const loadRequest = useMemo<EntityLoadRequest<TEntity>>(() => {\n const built: EntityLoadRequest<TEntity> = {\n page: request.currentPage,\n pageSize: request.pageSize,\n };\n // An empty group serializes to nothing; omit it rather than send a filter\n // that matches everything.\n if (rsql !== '' && effectiveFiltering) {\n built.filtering = [effectiveFiltering];\n }\n if (sort !== '' && request.sorting) {\n built.sorting = [request.sorting];\n }\n return built;\n }, [\n request.currentPage,\n request.pageSize,\n effectiveFiltering,\n request.sorting,\n rsql,\n sort,\n ]);\n\n // Falls back to a per-instance id so an un-scoped list never collides with\n // another in the shared cache — correct, just unshared (see the type doc).\n const instanceId = useId();\n const scope = queryKey ?? [instanceId];\n\n const query = useQuery<EntityPage<TEntity>, EntifixError>({\n enabled,\n /**\n * Page, size, filter and sort are all part of the key, so every one of them\n * is a *different* query — and without this, `data` goes undefined the\n * instant any of them changes. The table would then see `items.length === 0`\n * with `isFetching` true, read it as a first load, and blank populated rows\n * to skeleton on every pagination click: the grey flash the loading contract\n * exists to prevent. Keeping the previous page is what makes \"skeleton only\n * on first load\" true rather than merely intended.\n */\n placeholderData: keepPreviousData,\n queryKey: [\n ...scope,\n 'load',\n request.currentPage,\n request.pageSize,\n rsql,\n sort,\n ],\n queryFn: () =>\n Effect.runPromise(\n Effect.provide(\n ucRef.current,\n ctxRef.current.pipe(\n Context.add(\n EntityLoadRequestTag,\n // The tag carries no generic, so the entity-typed request crosses\n // it — the same cast `loadUCFactory` makes reading it back.\n loadRequest as unknown as EntityLoadRequest,\n ),\n ),\n ),\n ),\n });\n\n const onPageChange = useCallback((newPage: number) => {\n dispatch({ set: { currentPage: newPage } });\n }, []);\n\n const onPageSizeChange = useCallback((newPageSize: number) => {\n dispatch({ set: { pageSize: newPageSize, currentPage: 1 } });\n }, []);\n\n // Narrowing the result set invalidates the page number: page 4 of the old\n // result is very likely past the end of the new one.\n const onFilteringChange = useCallback((filtering: FilterGroup<TEntity>) => {\n dispatch({ set: { filtering, currentPage: 1 } });\n }, []);\n\n const onSortingChange = useCallback((sorting: EntitySorting<TEntity>) => {\n dispatch({ set: { sorting, currentPage: 1 } });\n }, []);\n\n return {\n isLoading: query.isFetching,\n items: query.data?.items ?? ([] as Array<TEntity>),\n totalItems: query.data?.total ?? 0,\n currentPage: request.currentPage,\n pageSize: request.pageSize,\n error: query.error ?? undefined,\n filtering: request.filtering,\n sorting: request.sorting,\n onPageChange,\n onPageSizeChange,\n onFilteringChange,\n onSortingChange,\n };\n}\n"],"names":["EntityLoadRequestTag","serializeRsql","serializeSort","keepPreviousData","useQuery","Context","Effect","useCallback","useEffect","useId","useMemo","useReducer","useRef","combineFilterGroups","DEFAULT_PAGE_SIZE","requestReducer","state","action","set","initialRequest","pageSize","currentPage","filtering","undefined","sorting","useDataLoading","uc","ctx","initialPageSize","queryKey","enabled","baseFiltering","query","request","dispatch","ucRef","ctxRef","current","effectiveFiltering","rsql","sort","loadRequest","built","page","instanceId","scope","placeholderData","queryFn","runPromise","provide","pipe","add","onPageChange","newPage","onPageSizeChange","newPageSize","onFilteringChange","onSortingChange","isLoading","isFetching","items","data","totalItems","total","error"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { EntityLoadRequestTag } from '@entifix/business';
|
|
2
|
+
import { EntifixError, Entity, EntityPage, EntitySorting, FilterGroup } from '@entifix/core';
|
|
3
|
+
import { Context, Effect } from 'effect';
|
|
4
|
+
export interface UseDataLoadingState<T extends Entity> {
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
items: Array<T>;
|
|
7
|
+
totalItems: number;
|
|
8
|
+
currentPage: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
error: EntifixError | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The filtering and sorting currently *applied* — not a draft. The controls
|
|
13
|
+
* commit through `onFilteringChange`/`onSortingChange`, so every value here
|
|
14
|
+
* has already been requested from the server.
|
|
15
|
+
*/
|
|
16
|
+
filtering: FilterGroup<T> | undefined;
|
|
17
|
+
sorting: EntitySorting<T> | undefined;
|
|
18
|
+
}
|
|
19
|
+
export type UseDataLoadingAction<T extends Entity> = {
|
|
20
|
+
set: Partial<UseDataLoadingState<T>>;
|
|
21
|
+
};
|
|
22
|
+
export interface UseDataLoadingOptions<TEntity extends Entity, TContext> {
|
|
23
|
+
uc: Effect.Effect<EntityPage<TEntity>, EntifixError, EntityLoadRequestTag | TContext>;
|
|
24
|
+
ctx: Context.Context<TContext>;
|
|
25
|
+
/**
|
|
26
|
+
* Page size for the first load; defaults to 10. Callers that need the whole
|
|
27
|
+
* set rather than a page — a relation picker, say — raise this.
|
|
28
|
+
*/
|
|
29
|
+
initialPageSize?: number;
|
|
30
|
+
/**
|
|
31
|
+
* The stable prefix that scopes this list in the shared query cache — pass
|
|
32
|
+
* {@link entityQueryScope}`(Ctor)` so refetches, optimistic writes, and
|
|
33
|
+
* reactive invalidation of the same entity all address one cache entry.
|
|
34
|
+
* Omit it and the hook falls back to a per-instance id: correct in isolation
|
|
35
|
+
* (no cross-list collisions) but unshared, so nothing outside this hook can
|
|
36
|
+
* invalidate it.
|
|
37
|
+
*/
|
|
38
|
+
queryKey?: ReadonlyArray<unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* `false` holds the request back without unmounting the hook — what a picker's
|
|
41
|
+
* browse dialog needs, since a list nobody has opened yet must not be fetched.
|
|
42
|
+
* Defaults to `true`, so every existing caller loads on mount as before.
|
|
43
|
+
*/
|
|
44
|
+
enabled?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A standing restriction ANDed into every request and never shown in the filter
|
|
47
|
+
* panel — "these are the rows this caller may see at all", as opposed to the
|
|
48
|
+
* filter the user is editing. A link picker narrowing what may be assigned uses
|
|
49
|
+
* it; so would a listing scoped to one tenant.
|
|
50
|
+
*/
|
|
51
|
+
baseFiltering?: FilterGroup<TEntity>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=use-data-loading.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-data-loading.types.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-data-loading/use-data-loading.types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EACL,YAAY,EACZ,MAAM,EACN,UAAU,EACV,aAAa,EACb,WAAW,EACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,YAAY,GAAG,SAAS,CAAC;IAChC;;;;OAIG;IACH,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IACtC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,MAAM,IAAI;IACnD,GAAG,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAAE,QAAQ;IACrE,EAAE,EAAE,MAAM,CAAC,MAAM,CACf,UAAU,CAAC,OAAO,CAAC,EACnB,YAAY,EACZ,oBAAoB,GAAG,QAAQ,CAChC,CAAC;IACF,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CACtC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/use-data-loading/use-data-loading.types.ts"],"sourcesContent":["import { EntityLoadRequestTag } from '@entifix/business';\nimport {\n EntifixError,\n Entity,\n EntityPage,\n EntitySorting,\n FilterGroup,\n} from '@entifix/core';\nimport { Context, Effect } from 'effect';\n\nexport interface UseDataLoadingState<T extends Entity> {\n isLoading: boolean;\n items: Array<T>;\n totalItems: number;\n currentPage: number;\n pageSize: number;\n error: EntifixError | undefined;\n /**\n * The filtering and sorting currently *applied* — not a draft. The controls\n * commit through `onFilteringChange`/`onSortingChange`, so every value here\n * has already been requested from the server.\n */\n filtering: FilterGroup<T> | undefined;\n sorting: EntitySorting<T> | undefined;\n}\n\nexport type UseDataLoadingAction<T extends Entity> = {\n set: Partial<UseDataLoadingState<T>>;\n};\n\nexport interface UseDataLoadingOptions<TEntity extends Entity, TContext> {\n uc: Effect.Effect<\n EntityPage<TEntity>,\n EntifixError,\n EntityLoadRequestTag | TContext\n >;\n ctx: Context.Context<TContext>;\n /**\n * Page size for the first load; defaults to 10. Callers that need the whole\n * set rather than a page — a relation picker, say — raise this.\n */\n initialPageSize?: number;\n /**\n * The stable prefix that scopes this list in the shared query cache — pass\n * {@link entityQueryScope}`(Ctor)` so refetches, optimistic writes, and\n * reactive invalidation of the same entity all address one cache entry.\n * Omit it and the hook falls back to a per-instance id: correct in isolation\n * (no cross-list collisions) but unshared, so nothing outside this hook can\n * invalidate it.\n */\n queryKey?: ReadonlyArray<unknown>;\n /**\n * `false` holds the request back without unmounting the hook — what a picker's\n * browse dialog needs, since a list nobody has opened yet must not be fetched.\n * Defaults to `true`, so every existing caller loads on mount as before.\n */\n enabled?: boolean;\n /**\n * A standing restriction ANDed into every request and never shown in the filter\n * panel — \"these are the rows this caller may see at all\", as opposed to the\n * filter the user is editing. A link picker narrowing what may be assigned uses\n * it; so would a listing scoped to one tenant.\n */\n baseFiltering?: FilterGroup<TEntity>;\n}\n"],"names":[],"mappings":"AA8BA,WAkCC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/index.ts"],"names":[],"mappings":"AAAA,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/use-entity-form/index.ts"],"sourcesContent":["export * from './use-entity-form.helpers.js';\nexport * from './use-entity-form.js';\nexport * from './use-entity-form.types.js';\n"],"names":[],"mappings":"AAAA,cAAc,+BAA+B;AAC7C,cAAc,uBAAuB;AACrC,cAAc,6BAA6B"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Entity } from '@entifix/core';
|
|
2
|
+
import type { UseEntityFormOptions, UseEntityFormResult } from './use-entity-form.types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The write half of an entity form: holds the string draft and reports
|
|
5
|
+
* metadata-derived validation, the counterpart to {@link useEntityRecord}
|
|
6
|
+
* (which reads the record it seeds from).
|
|
7
|
+
*
|
|
8
|
+
* It stays out of the transport: it neither fetches nor saves. `onSubmit`
|
|
9
|
+
* receives the validated draft, and the per-entity wrapper reconstructs the
|
|
10
|
+
* domain entity and calls the mutation. The result plugs straight into the
|
|
11
|
+
* agnostic `EntityForm` (`values`/`onFieldChange`/`errors`/`onSubmit`).
|
|
12
|
+
*
|
|
13
|
+
* TanStack Form owns the draft and the submission lifecycle; that is an
|
|
14
|
+
* implementation detail, and the returned shape is deliberately the plain
|
|
15
|
+
* `values`/`errors`/`setField`/`submit`/`isDirty` facade so `EntityForm` — which
|
|
16
|
+
* renders native inputs and knows nothing about any form library — stays
|
|
17
|
+
* agnostic. Fields are therefore never registered through `form.Field`; a
|
|
18
|
+
* form-level validator reporting `{ fields }` reaches them anyway, because
|
|
19
|
+
* `FormApi` creates meta for any field name an error arrives for.
|
|
20
|
+
*
|
|
21
|
+
* `revalidateLogic` is what makes a pristine form quiet: nothing validates until
|
|
22
|
+
* the first submit, and every keystroke revalidates after it. Running one
|
|
23
|
+
* validator under a single cause also keeps a fixed field from holding a stale
|
|
24
|
+
* message left behind by another cause.
|
|
25
|
+
*
|
|
26
|
+
* The seed applies once, so a caller that loads its record after mount keys the
|
|
27
|
+
* form by the record id to reseed — the same convention `EntityForm`'s wrappers
|
|
28
|
+
* already follow.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useEntityForm<TEntity extends Entity>({ entityConstructor, entity, draft, fields, schema, validate, validationMessages, translateKey, onSubmit, }: UseEntityFormOptions<TEntity>): UseEntityFormResult;
|
|
31
|
+
//# sourceMappingURL=use-entity-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-entity-form.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,MAAM,EAIZ,MAAM,eAAe,CAAC;AAWvB,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAAE,EACpD,iBAAiB,EACjB,MAAM,EACN,KAAK,EACL,MAAM,EACN,MAAM,EACN,QAAQ,EACR,kBAAkB,EAClB,YAAY,EACZ,QAAQ,GACT,EAAE,oBAAoB,CAAC,OAAO,CAAC,GAAG,mBAAmB,CAyLrD"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { type EntityDraft, type EntityDraftValue, type EntityFieldDescriptor, type StandardSchemaV1 } from '@entifix/core';
|
|
2
|
+
/**
|
|
3
|
+
* The value a field seeds with from a record.
|
|
4
|
+
*
|
|
5
|
+
* A scalar seeds as a **string**: links with their foreign key(s), dates with a
|
|
6
|
+
* `yyyy-mm-dd` value a `date` input accepts, everything else stringified.
|
|
7
|
+
*
|
|
8
|
+
* The array branch is explicit rather than left to `String(raw)`, even though
|
|
9
|
+
* the two produce the same characters today. `String(['a','b'])` is `'a,b'` by
|
|
10
|
+
* way of `Array.prototype.toString`, which is an accident: it was doing the
|
|
11
|
+
* work of a `scalarCollection` seed before the type existed, and
|
|
12
|
+
* `reconstructEntity` was handing the same string straight back as a `string`.
|
|
13
|
+
* Both halves were wrong in the same direction, so the fixed-point spec could
|
|
14
|
+
* not see it. Now the join is declared here and the split is declared there,
|
|
15
|
+
* and they are inverses on purpose.
|
|
16
|
+
*
|
|
17
|
+
* A **`composition`** is the one member that does not seed as a string, because
|
|
18
|
+
* it has no lossless string form at all — its rows seed as row drafts, each
|
|
19
|
+
* with a freshly minted key. That key is the only thing here the record does not
|
|
20
|
+
* supply, and it is minted at seed time rather than at render so that the rows a
|
|
21
|
+
* form starts with are already addressable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function seedFieldValue(descriptor: EntityFieldDescriptor, entity: unknown): EntityDraftValue;
|
|
24
|
+
/** Builds the initial draft for a record (or an empty one for a create). */
|
|
25
|
+
export declare function seedEntityDraft(descriptors: readonly EntityFieldDescriptor[], entity: unknown): EntityDraft;
|
|
26
|
+
/**
|
|
27
|
+
* Restores a persisted draft **over** a freshly seeded one, rather than in place
|
|
28
|
+
* of it.
|
|
29
|
+
*
|
|
30
|
+
* A draft outlives the entity that wrote it — it sits in IndexedDB across
|
|
31
|
+
* refreshes and deployments — so the two can disagree by the time it comes back.
|
|
32
|
+
* Replacing the seed outright is how that disagreement becomes a bug the user
|
|
33
|
+
* sees: a member added since the draft was written has no entry, reaches the form
|
|
34
|
+
* engine as `undefined`, and its input flips from controlled to uncontrolled
|
|
35
|
+
* mid-render. A member since removed goes the other way, surviving as a value
|
|
36
|
+
* nothing renders and `reconstructEntity` cannot assign.
|
|
37
|
+
*
|
|
38
|
+
* So the seed decides the *keys* and the draft decides the *values*: a name the
|
|
39
|
+
* entity no longer declares is dropped, and a name it declares that the draft
|
|
40
|
+
* lacks keeps its seeded value.
|
|
41
|
+
*
|
|
42
|
+
* **A value is restored only in the shape its member can hold**, which is now
|
|
43
|
+
* two shapes rather than one. A scalar takes a string; a `composition` takes a
|
|
44
|
+
* readable row list. Anything else is dropped back to its seed — a member that
|
|
45
|
+
* changed type between the write and the read, or a row list from a build that
|
|
46
|
+
* did not carry keys, restores as the record's own value instead of as
|
|
47
|
+
* something no editor can render. This is per member, so one unreadable entry
|
|
48
|
+
* never costs the user the rest of the form.
|
|
49
|
+
*
|
|
50
|
+
* This is the shape guard at the one place the shape is actually known. The
|
|
51
|
+
* store-level version check (`drafts-state.ts`) can only decide that a whole
|
|
52
|
+
* generation of drafts is unreadable; it cannot know which members one entity
|
|
53
|
+
* declares.
|
|
54
|
+
*/
|
|
55
|
+
export declare function restoreEntityDraft(descriptors: readonly EntityFieldDescriptor[], seed: EntityDraft, persisted: EntityDraft | undefined): EntityDraft;
|
|
56
|
+
/** The slice of a form field's state this module reads: its error list. */
|
|
57
|
+
export interface FieldErrorMeta {
|
|
58
|
+
errors?: readonly unknown[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Flattens the form engine's per-field state into the `name → message` map the
|
|
62
|
+
* agnostic `EntityForm` renders. Only the first message per field survives: a
|
|
63
|
+
* row shows one line, and the metadata rules already stop at the first failure.
|
|
64
|
+
*
|
|
65
|
+
* Kept structural rather than typed against the engine so the rest of this
|
|
66
|
+
* module stays independent of which form library backs the hook.
|
|
67
|
+
*/
|
|
68
|
+
export declare function readFieldErrors(fieldMeta: Partial<Record<string, FieldErrorMeta | undefined>>): Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* The four metadata-derived messages, already localized. Taken as an argument
|
|
71
|
+
* rather than built here so this stays a pure function — and because the
|
|
72
|
+
* sentence cannot be assembled from a label plus a hardcoded suffix: word order
|
|
73
|
+
* differs between locales.
|
|
74
|
+
*/
|
|
75
|
+
export interface EntityDraftMessages {
|
|
76
|
+
required(field: string): string;
|
|
77
|
+
number(field: string): string;
|
|
78
|
+
date(field: string): string;
|
|
79
|
+
option(field: string): string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validates a draft against what the metadata implies — `required` members must
|
|
83
|
+
* be filled, and a filled `number`/`date`/`enum` must be well-formed. Read-only
|
|
84
|
+
* members are skipped entirely; a relation is checked for presence but never
|
|
85
|
+
* for format, since its draft value is a foreign key this side cannot resolve.
|
|
86
|
+
*
|
|
87
|
+
* Metadata is only the first of three rule sources; {@link composeEntityFormErrors}
|
|
88
|
+
* is what layers a schema and the caller's own rules over this.
|
|
89
|
+
*/
|
|
90
|
+
export declare function validateEntityDraft(descriptors: readonly EntityFieldDescriptor[], values: EntityDraft, messages: EntityDraftMessages): Record<string, string>;
|
|
91
|
+
/**
|
|
92
|
+
* Runs a Standard Schema over the draft and sorts its issues into per-field
|
|
93
|
+
* messages plus a form-level one (an issue with no path — a cross-field rule).
|
|
94
|
+
*
|
|
95
|
+
* **Schemas are authored against the string draft**, so they coerce
|
|
96
|
+
* (`z.coerce.number()`) rather than expect typed values: the draft is what the
|
|
97
|
+
* inputs round-trip, and the same schema then validates exactly what the user
|
|
98
|
+
* sees. Only the first issue per field survives, matching how a row renders.
|
|
99
|
+
*
|
|
100
|
+
* Synchronous rules only. An async schema returns a Promise the form's
|
|
101
|
+
* synchronous validation pass cannot wait on, so it is rejected loudly rather
|
|
102
|
+
* than silently passing.
|
|
103
|
+
*/
|
|
104
|
+
export declare function readSchemaIssues(schema: StandardSchemaV1, values: EntityDraft, translateIssue: (message: string, field: string | undefined) => string): {
|
|
105
|
+
fields: Record<string, string>;
|
|
106
|
+
form?: string;
|
|
107
|
+
};
|
|
108
|
+
/** Everything {@link composeEntityFormErrors} needs to judge one draft. */
|
|
109
|
+
export interface ComposeEntityFormErrorsOptions {
|
|
110
|
+
descriptors: readonly EntityFieldDescriptor[];
|
|
111
|
+
values: EntityDraft;
|
|
112
|
+
messages: EntityDraftMessages;
|
|
113
|
+
schema?: StandardSchemaV1;
|
|
114
|
+
translateIssue: (message: string, field: string | undefined) => string;
|
|
115
|
+
validate?: (values: EntityDraft) => Record<string, string>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The single composition point for a draft's errors: metadata rules first, then
|
|
119
|
+
* the entity's schema, then the caller's `validate` callback. Later sources win
|
|
120
|
+
* on conflict, so a caller can always override a message it disagrees with.
|
|
121
|
+
*/
|
|
122
|
+
export declare function composeEntityFormErrors({ descriptors, values, messages, schema, translateIssue, validate, }: ComposeEntityFormErrorsOptions): {
|
|
123
|
+
fields: Record<string, string>;
|
|
124
|
+
form?: string;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* What a caller who supplies no {@link ValidationMessages} gets: the catalog key
|
|
128
|
+
* itself, with the field appended.
|
|
129
|
+
*
|
|
130
|
+
* Honest rather than English — there is no catalog in this package to resolve
|
|
131
|
+
* against — and visible enough in a form that a host notices it has wired no
|
|
132
|
+
* copy. It sits beside the validator rather than in `.types.ts`, which this
|
|
133
|
+
* repository keeps type-only.
|
|
134
|
+
*/
|
|
135
|
+
export declare const DEFAULT_VALIDATION_MESSAGES: EntityDraftMessages;
|
|
136
|
+
//# sourceMappingURL=use-entity-form.helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-entity-form.helpers.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-entity-form/use-entity-form.helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAU1B,KAAK,gBAAgB,EAEtB,MAAM,eAAe,CAAC;AAwCvB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,qBAAqB,EACjC,MAAM,EAAE,OAAO,GACd,gBAAgB,CAKlB;AAqDD,4EAA4E;AAC5E,wBAAgB,eAAe,CAC7B,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,OAAO,GACd,WAAW,CAMb;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,WAAW,GAAG,SAAS,GACjC,WAAW,CAeb;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC,CAAC,GAC7D,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,qBAAqB,EAAE,EAC7C,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,mBAAmB,GAC5B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAkHD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,gBAAgB,EACxB,MAAM,EAAE,WAAW,EACnB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GACrE;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBnD;AAED,2EAA2E;AAC3E,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC9C,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,CAAC;IACvE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,WAAW,EACX,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,QAAQ,GACT,EAAE,8BAA8B,GAAG;IAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAUA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,EAAE,mBAKzC,CAAC"}
|