@contember/bindx-react 0.1.37 → 0.1.38
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/dist/hooks/useEntity.d.ts +10 -0
- package/dist/hooks/useEntity.d.ts.map +1 -1
- package/dist/hooks/useEntityList.d.ts +9 -0
- package/dist/hooks/useEntityList.d.ts.map +1 -1
- package/dist/jsx/components/Entity.d.ts +7 -0
- package/dist/jsx/components/Entity.d.ts.map +1 -1
- package/dist/jsx/components/EntityList.d.ts +7 -1
- package/dist/jsx/components/EntityList.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/hooks/useEntity.ts +38 -9
- package/src/hooks/useEntityList.ts +38 -5
- package/src/jsx/components/Entity.tsx +18 -2
- package/src/jsx/components/EntityList.tsx +14 -2
|
@@ -27,6 +27,7 @@ interface UseEntityResultBase {
|
|
|
27
27
|
export type LoadingEntityResult = UseEntityResultBase & {
|
|
28
28
|
readonly $status: 'loading';
|
|
29
29
|
readonly $isLoading: true;
|
|
30
|
+
readonly $isRefetching: false;
|
|
30
31
|
readonly $isError: false;
|
|
31
32
|
readonly $isNotFound: false;
|
|
32
33
|
readonly $error: null;
|
|
@@ -35,6 +36,7 @@ export type LoadingEntityResult = UseEntityResultBase & {
|
|
|
35
36
|
export type ErrorEntityResult = UseEntityResultBase & {
|
|
36
37
|
readonly $status: 'error';
|
|
37
38
|
readonly $isLoading: false;
|
|
39
|
+
readonly $isRefetching: false;
|
|
38
40
|
readonly $isError: true;
|
|
39
41
|
readonly $isNotFound: false;
|
|
40
42
|
readonly $error: FieldError;
|
|
@@ -43,6 +45,7 @@ export type ErrorEntityResult = UseEntityResultBase & {
|
|
|
43
45
|
export type NotFoundEntityResult = UseEntityResultBase & {
|
|
44
46
|
readonly $status: 'not_found';
|
|
45
47
|
readonly $isLoading: false;
|
|
48
|
+
readonly $isRefetching: false;
|
|
46
49
|
readonly $isError: false;
|
|
47
50
|
readonly $isNotFound: true;
|
|
48
51
|
readonly $error: null;
|
|
@@ -50,10 +53,17 @@ export type NotFoundEntityResult = UseEntityResultBase & {
|
|
|
50
53
|
};
|
|
51
54
|
/**
|
|
52
55
|
* Ready state — full EntityAccessor with status metadata.
|
|
56
|
+
*
|
|
57
|
+
* `$isRefetching` is `true` while a background re-fetch is in flight
|
|
58
|
+
* (triggered by a `queryKey` change while ready data is already present).
|
|
59
|
+
* The accessor identity and field values remain stable until the new data
|
|
60
|
+
* arrives, so the subtree does not unmount — useful for stale-while-revalidate
|
|
61
|
+
* indicators (subtle spinner, "stale" badge, etc.).
|
|
53
62
|
*/
|
|
54
63
|
export type ReadyEntityResult<TEntity extends object, TSelected extends object = TEntity> = UseEntityResultBase & EntityAccessor<TEntity, TSelected> & {
|
|
55
64
|
readonly $status: 'ready';
|
|
56
65
|
readonly $isLoading: false;
|
|
66
|
+
readonly $isRefetching: boolean;
|
|
57
67
|
readonly $isError: false;
|
|
58
68
|
readonly $isNotFound: false;
|
|
59
69
|
readonly $error: null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEntity.d.ts","sourceRoot":"","sources":["../../src/hooks/useEntity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,SAAS,EACT,iBAAiB,EAEjB,cAAc,EACd,aAAa,EACb,UAAU,EAIV,YAAY,EACZ,cAAc,EACd,SAAS,EACT,cAAc,EACd,MAAM,kBAAkB,CAAA;AAgBzB;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,sFAAsF;IACtF,EAAE,EAAE,iBAAiB,CAAA;IACrB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAMD;;;GAGG;AACH,UAAU,mBAAmB;IAC5B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,MAAM,IAAI,IAAI,CAAA;CACd;AAED,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IACvD,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG;IACxD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED
|
|
1
|
+
{"version":3,"file":"useEntity.d.ts","sourceRoot":"","sources":["../../src/hooks/useEntity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,SAAS,EACT,iBAAiB,EAEjB,cAAc,EACd,aAAa,EACb,UAAU,EAIV,YAAY,EACZ,cAAc,EACd,SAAS,EACT,cAAc,EACd,MAAM,kBAAkB,CAAA;AAgBzB;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,sFAAsF;IACtF,EAAE,EAAE,iBAAiB,CAAA;IACrB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAMD;;;GAGG;AACH,UAAU,mBAAmB;IAC5B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,MAAM,IAAI,IAAI,CAAA;CACd;AAED,MAAM,MAAM,mBAAmB,GAAG,mBAAmB,GAAG;IACvD,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,mBAAmB,GAAG;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG;IACxD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,aAAa,EAAE,KAAK,CAAA;IAC7B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;IACrB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM,GAAG,OAAO,IACvF,mBAAmB,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG;IAC1D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;IAC/B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAA;IAC3B,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CACrB,CAAA;AAEF;;;GAGG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,MAAM,EAAE,SAAS,SAAS,MAAM,GAAG,OAAO,IACnF,mBAAmB,GACnB,iBAAiB,GACjB,oBAAoB,GACpB,iBAAiB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;AAMxC;;GAEG;AACH,wBAAgB,SAAS,CACxB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACvC,MAAM,SAAS,SAAS,CAAC,QAAQ,CAAC,EAClC,OAAO,SAAS,MAAM,EAEtB,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAC3B,OAAO,EAAE,gBAAgB,GAAG;IAAE,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EACxD,OAAO,EAAE,cAAc,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,GAChE,eAAe,CAAC,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAA;AAE7D;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,SAAS,MAAM,EACxF,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAC3B,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,GACtD,eAAe,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAA;AAEnD;;;GAGG;AACH,wBAAgB,SAAS,CACxB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,gBAAgB,GAAG;IAAE,SAAS,EAAE,aAAa,CAAA;CAAE,GACtD,eAAe,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA"}
|
|
@@ -28,18 +28,27 @@ interface EntityListResultBase {
|
|
|
28
28
|
export type LoadingEntityListResult = EntityListResultBase & {
|
|
29
29
|
readonly $status: 'loading';
|
|
30
30
|
readonly $isLoading: true;
|
|
31
|
+
readonly $isRefetching: false;
|
|
31
32
|
readonly $isError: false;
|
|
32
33
|
readonly $error: null;
|
|
33
34
|
};
|
|
34
35
|
export type ErrorEntityListResult = EntityListResultBase & {
|
|
35
36
|
readonly $status: 'error';
|
|
36
37
|
readonly $isLoading: false;
|
|
38
|
+
readonly $isRefetching: false;
|
|
37
39
|
readonly $isError: true;
|
|
38
40
|
readonly $error: FieldError;
|
|
39
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* `$isRefetching` is `true` while a background re-fetch is in flight
|
|
44
|
+
* (triggered by a `queryKey` change while ready data is already present).
|
|
45
|
+
* The accessor identity stays stable so the subtree does not unmount —
|
|
46
|
+
* stale-while-revalidate semantics.
|
|
47
|
+
*/
|
|
40
48
|
export type ReadyEntityListResult<T extends object> = EntityListResultBase & {
|
|
41
49
|
readonly $status: 'ready';
|
|
42
50
|
readonly $isLoading: false;
|
|
51
|
+
readonly $isRefetching: boolean;
|
|
43
52
|
readonly $isError: false;
|
|
44
53
|
readonly $error: null;
|
|
45
54
|
readonly $isDirty: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEntityList.d.ts","sourceRoot":"","sources":["../../src/hooks/useEntityList.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAkB,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AASrK;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAChC,wBAAwB;IACxB,OAAO,CAAC,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;IAC5C,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAMD,UAAU,oBAAoB;IAC7B,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC5B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/C;AAED,MAAM,MAAM,uBAAuB,GAAG,oBAAoB,GAAG;IAC5D,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG;IAC1D,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,IAAI,oBAAoB,GAAG;IAC5E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;IACxC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,IAC7C,uBAAuB,GACvB,qBAAqB,GACrB,qBAAqB,CAAC,CAAC,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"useEntityList.d.ts","sourceRoot":"","sources":["../../src/hooks/useEntityList.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,6 +19,13 @@ interface EntityBaseProps<TRoleMap extends Record<string, object>> {
|
|
|
19
19
|
interface EntityByProps<TRoleMap extends Record<string, object>> extends EntityBaseProps<TRoleMap> {
|
|
20
20
|
/** Unique field(s) to identify the entity (e.g., { id: '...' } or { slug: '...' }) */
|
|
21
21
|
by: EntityUniqueWhere;
|
|
22
|
+
/**
|
|
23
|
+
* Cache-invalidation key. Changing this value re-fetches the entity from the
|
|
24
|
+
* server without unmounting the subtree (stale-while-revalidate). Use this
|
|
25
|
+
* instead of `key={...}` when external mutations (e.g. workflow RPC) need
|
|
26
|
+
* the bindx view refreshed but UI state (scroll, dialogs, drafts) must survive.
|
|
27
|
+
*/
|
|
28
|
+
queryKey?: string;
|
|
22
29
|
create?: never;
|
|
23
30
|
onPersisted?: never;
|
|
24
31
|
/** Loading fallback */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../../../src/jsx/components/Entity.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAKrH,OAAO,KAAK,EAAkB,SAAS,EAAiB,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAiB,KAAK,UAAU,EAAgF,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAI1M;;;GAGG;AACH,UAAU,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChE,kCAAkC;IAClC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IACxE,qBAAqB;IACrB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;CAC9C;AAED;;GAEG;AACH,UAAU,aAAa,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACjG,sFAAsF;IACtF,EAAE,EAAE,iBAAiB,CAAA;IACrB,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,WAAW,CAAC,EAAE,KAAK,CAAA;IACnB,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;GAEG;AACH,UAAU,iBAAiB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACrG,EAAE,CAAC,EAAE,KAAK,CAAA;IACV,8DAA8D;IAC9D,MAAM,EAAE,IAAI,CAAA;IACZ,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,KAAK,CAAA;IACf,QAAQ,CAAC,EAAE,KAAK,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IACrF,aAAa,CAAC,QAAQ,CAAC,GACvB,iBAAiB,CAAC,QAAQ,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"Entity.d.ts","sourceRoot":"","sources":["../../../src/jsx/components/Entity.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAuE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAKrH,OAAO,KAAK,EAAkB,SAAS,EAAiB,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,iBAAiB,EAAiB,KAAK,UAAU,EAAgF,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAI1M;;;GAGG;AACH,UAAU,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChE,kCAAkC;IAClC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3B,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,KAAK,KAAK,CAAC,SAAS,CAAA;IACxE,qBAAqB;IACrB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;CAC9C;AAED;;GAEG;AACH,UAAU,aAAa,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACjG,sFAAsF;IACtF,EAAE,EAAE,iBAAiB,CAAA;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,KAAK,CAAA;IACd,WAAW,CAAC,EAAE,KAAK,CAAA;IACnB,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAC1B;AAED;;GAEG;AACH,UAAU,iBAAiB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAE,SAAQ,eAAe,CAAC,QAAQ,CAAC;IACrG,EAAE,CAAC,EAAE,KAAK,CAAA;IACV,8DAA8D;IAC9D,MAAM,EAAE,IAAI,CAAA;IACZ,wEAAwE;IACxE,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,OAAO,CAAC,EAAE,KAAK,CAAA;IACf,QAAQ,CAAC,EAAE,KAAK,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IACrF,aAAa,CAAC,QAAQ,CAAC,GACvB,iBAAiB,CAAC,QAAQ,CAAC,CAAA;AAsP9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,iBAAS,UAAU,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC1D,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,GAC1B,YAAY,GAAG,IAAI,CA2BrB;AAGD,eAAO,MAAM,MAAM,EAAkC,OAAO,UAAU,CAAA"}
|
|
@@ -15,6 +15,12 @@ export interface EntityListProps<TRoleMap extends Record<string, object> = Recor
|
|
|
15
15
|
limit?: number;
|
|
16
16
|
/** Optional offset */
|
|
17
17
|
offset?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Cache-invalidation key. Changing this value re-fetches the list without
|
|
20
|
+
* unmounting the subtree (stale-while-revalidate). Use this when external
|
|
21
|
+
* mutations need the bindx view refreshed but UI state must survive.
|
|
22
|
+
*/
|
|
23
|
+
queryKey?: string;
|
|
18
24
|
/** Render function receiving typed entity accessor with direct field access */
|
|
19
25
|
children: (entity: EntityAccessor<CommonEntity<TRoleMap>>, index: number) => React.ReactNode;
|
|
20
26
|
/** Loading fallback */
|
|
@@ -42,7 +48,7 @@ export interface EntityListProps<TRoleMap extends Record<string, object> = Recor
|
|
|
42
48
|
* </EntityList>
|
|
43
49
|
* ```
|
|
44
50
|
*/
|
|
45
|
-
declare function EntityListComponent<TRoleMap extends Record<string, object>>({ entity, filter, orderBy, limit, offset, children, loading, error: errorFallback, empty, }: EntityListProps<TRoleMap>): ReactElement | null;
|
|
51
|
+
declare function EntityListComponent<TRoleMap extends Record<string, object>>({ entity, filter, orderBy, limit, offset, queryKey: userQueryKey, children, loading, error: errorFallback, empty, }: EntityListProps<TRoleMap>): ReactElement | null;
|
|
46
52
|
export declare const EntityList: typeof EntityListComponent;
|
|
47
53
|
export {};
|
|
48
54
|
//# sourceMappingURL=EntityList.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/jsx/components/EntityList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAQ,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAG3H,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGjD;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChG,kCAAkC;IAClC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3B,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5C,2DAA2D;IAC3D,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAA;IAC1D,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAA;IAC5F,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,qBAAqB;IACrB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;IAC9C,oDAAoD;IACpD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAS,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EACrE,MAAM,EACN,MAAM,EACN,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAQ,EACR,OAAO,EACP,KAAK,EAAE,aAAa,EACpB,KAAK,GACL,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,YAAY,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"EntityList.d.ts","sourceRoot":"","sources":["../../../src/jsx/components/EntityList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAQ,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AACtD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAG3H,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAGjD;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAChG,kCAAkC;IAClC,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAA;IAC3B,kEAAkE;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5C,2DAA2D;IAC3D,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAA;IAC1D,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAA;IAC5F,uBAAuB;IACvB,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,qBAAqB;IACrB,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,KAAK,CAAC,SAAS,CAAA;IAC9C,oDAAoD;IACpD,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAS,mBAAmB,CAAC,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EACrE,MAAM,EACN,MAAM,EACN,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAQ,EAAE,YAAY,EACtB,QAAQ,EACR,OAAO,EACP,KAAK,EAAE,aAAa,EACpB,KAAK,GACL,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,YAAY,GAAG,IAAI,CA8DjD;AAGD,eAAO,MAAM,UAAU,EAA2C,OAAO,mBAAmB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/bindx-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "Type-safe React data binding framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"react": ">=18.0.0"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@contember/bindx": "0.1.
|
|
19
|
-
"@contember/bindx-client": "0.1.
|
|
18
|
+
"@contember/bindx": "0.1.38",
|
|
19
|
+
"@contember/bindx-client": "0.1.38",
|
|
20
20
|
"@contember/graphql-client": "^2.1.0-beta.1",
|
|
21
21
|
"@radix-ui/primitive": "^1.1.3",
|
|
22
22
|
"@radix-ui/react-slot": "^1.2.4"
|
package/src/hooks/useEntity.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useEffect, useMemo, useCallback } from 'react'
|
|
1
|
+
import { useRef, useEffect, useMemo, useCallback, useState } from 'react'
|
|
2
2
|
import type {
|
|
3
3
|
EntityDef,
|
|
4
4
|
EntityUniqueWhere,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
EntityHandle,
|
|
19
19
|
resolveSelectionMeta,
|
|
20
20
|
buildQueryFromSelection,
|
|
21
|
-
|
|
21
|
+
refreshServerData,
|
|
22
22
|
setLoadState,
|
|
23
23
|
createLoadError,
|
|
24
24
|
} from '@contember/bindx'
|
|
@@ -63,6 +63,7 @@ interface UseEntityResultBase {
|
|
|
63
63
|
export type LoadingEntityResult = UseEntityResultBase & {
|
|
64
64
|
readonly $status: 'loading'
|
|
65
65
|
readonly $isLoading: true
|
|
66
|
+
readonly $isRefetching: false
|
|
66
67
|
readonly $isError: false
|
|
67
68
|
readonly $isNotFound: false
|
|
68
69
|
readonly $error: null
|
|
@@ -72,6 +73,7 @@ export type LoadingEntityResult = UseEntityResultBase & {
|
|
|
72
73
|
export type ErrorEntityResult = UseEntityResultBase & {
|
|
73
74
|
readonly $status: 'error'
|
|
74
75
|
readonly $isLoading: false
|
|
76
|
+
readonly $isRefetching: false
|
|
75
77
|
readonly $isError: true
|
|
76
78
|
readonly $isNotFound: false
|
|
77
79
|
readonly $error: FieldError
|
|
@@ -81,6 +83,7 @@ export type ErrorEntityResult = UseEntityResultBase & {
|
|
|
81
83
|
export type NotFoundEntityResult = UseEntityResultBase & {
|
|
82
84
|
readonly $status: 'not_found'
|
|
83
85
|
readonly $isLoading: false
|
|
86
|
+
readonly $isRefetching: false
|
|
84
87
|
readonly $isError: false
|
|
85
88
|
readonly $isNotFound: true
|
|
86
89
|
readonly $error: null
|
|
@@ -89,11 +92,18 @@ export type NotFoundEntityResult = UseEntityResultBase & {
|
|
|
89
92
|
|
|
90
93
|
/**
|
|
91
94
|
* Ready state — full EntityAccessor with status metadata.
|
|
95
|
+
*
|
|
96
|
+
* `$isRefetching` is `true` while a background re-fetch is in flight
|
|
97
|
+
* (triggered by a `queryKey` change while ready data is already present).
|
|
98
|
+
* The accessor identity and field values remain stable until the new data
|
|
99
|
+
* arrives, so the subtree does not unmount — useful for stale-while-revalidate
|
|
100
|
+
* indicators (subtle spinner, "stale" badge, etc.).
|
|
92
101
|
*/
|
|
93
102
|
export type ReadyEntityResult<TEntity extends object, TSelected extends object = TEntity> =
|
|
94
103
|
UseEntityResultBase & EntityAccessor<TEntity, TSelected> & {
|
|
95
104
|
readonly $status: 'ready'
|
|
96
105
|
readonly $isLoading: false
|
|
106
|
+
readonly $isRefetching: boolean
|
|
97
107
|
readonly $isError: false
|
|
98
108
|
readonly $isNotFound: false
|
|
99
109
|
readonly $error: null
|
|
@@ -236,6 +246,7 @@ export function useEntity(
|
|
|
236
246
|
|
|
237
247
|
// --- Data loading ---
|
|
238
248
|
const fetchingRef = useRef<string | null>(null)
|
|
249
|
+
const [isRefetching, setIsRefetching] = useState(false)
|
|
239
250
|
|
|
240
251
|
useEffect(() => {
|
|
241
252
|
// Check cache first
|
|
@@ -253,7 +264,17 @@ export function useEntity(
|
|
|
253
264
|
|
|
254
265
|
const abortController = new AbortController()
|
|
255
266
|
|
|
256
|
-
|
|
267
|
+
// Stale-while-revalidate: if we already have ready data for this entity,
|
|
268
|
+
// keep the ready state visible (no flicker) and only flag a background refetch.
|
|
269
|
+
// Otherwise, fall back to the explicit loading state.
|
|
270
|
+
const existing = store.getLoadState(entityType, id)
|
|
271
|
+
const haveReadyData = existing?.status === 'success' && store.hasEntity(entityType, id)
|
|
272
|
+
|
|
273
|
+
if (haveReadyData) {
|
|
274
|
+
setIsRefetching(true)
|
|
275
|
+
} else {
|
|
276
|
+
dispatcher.dispatch(setLoadState(entityType, id, 'loading'))
|
|
277
|
+
}
|
|
257
278
|
|
|
258
279
|
const fetchData = async (): Promise<void> => {
|
|
259
280
|
try {
|
|
@@ -269,8 +290,10 @@ export function useEntity(
|
|
|
269
290
|
if (result.type === 'get' && result.data === null) {
|
|
270
291
|
dispatcher.dispatch(setLoadState(entityType, id, 'not_found'))
|
|
271
292
|
} else if (result.type === 'get' && result.data) {
|
|
293
|
+
// Revalidation: advance the server baseline but keep local dirty
|
|
294
|
+
// edits intact (see EntitySnapshotStore.refreshServerData).
|
|
272
295
|
dispatcher.dispatch(
|
|
273
|
-
|
|
296
|
+
refreshServerData(entityType, id, result.data),
|
|
274
297
|
)
|
|
275
298
|
dispatcher.dispatch(setLoadState(entityType, id, 'success'))
|
|
276
299
|
}
|
|
@@ -282,6 +305,10 @@ export function useEntity(
|
|
|
282
305
|
dispatcher.dispatch(
|
|
283
306
|
setLoadState(entityType, id, 'error', createLoadError(normalizedError)),
|
|
284
307
|
)
|
|
308
|
+
} finally {
|
|
309
|
+
if (!abortController.signal.aborted) {
|
|
310
|
+
setIsRefetching(false)
|
|
311
|
+
}
|
|
285
312
|
}
|
|
286
313
|
}
|
|
287
314
|
|
|
@@ -324,20 +351,20 @@ export function useEntity(
|
|
|
324
351
|
// --- Build result ---
|
|
325
352
|
const result = useMemo((): UseEntityResult<object, object> => {
|
|
326
353
|
if (!loadState || loadState.status === 'loading' || (!snapshot && loadState.status === 'success')) {
|
|
327
|
-
return { $status: 'loading', $isLoading: true, $isError: false, $isNotFound: false, $error: null, id, $persist: persist, $reset: reset }
|
|
354
|
+
return { $status: 'loading', $isLoading: true, $isRefetching: false, $isError: false, $isNotFound: false, $error: null, id, $persist: persist, $reset: reset }
|
|
328
355
|
}
|
|
329
356
|
|
|
330
357
|
if (loadState.status === 'error') {
|
|
331
|
-
return { $status: 'error', $isLoading: false, $isError: true, $isNotFound: false, $error: loadState.error!, id, $persist: persist, $reset: reset }
|
|
358
|
+
return { $status: 'error', $isLoading: false, $isRefetching: false, $isError: true, $isNotFound: false, $error: loadState.error!, id, $persist: persist, $reset: reset }
|
|
332
359
|
}
|
|
333
360
|
|
|
334
361
|
if (loadState.status === 'not_found') {
|
|
335
|
-
return { $status: 'not_found', $isLoading: false, $isError: false, $isNotFound: true, $error: null, id, $persist: persist, $reset: reset }
|
|
362
|
+
return { $status: 'not_found', $isLoading: false, $isRefetching: false, $isError: false, $isNotFound: true, $error: null, id, $persist: persist, $reset: reset }
|
|
336
363
|
}
|
|
337
364
|
|
|
338
365
|
// Ready — layer status metadata on top of EntityHandle proxy
|
|
339
|
-
return createReadyResult(handle, persist, reset)
|
|
340
|
-
}, [snapshot, loadState, isPersisting, id, handle, persist, reset])
|
|
366
|
+
return createReadyResult(handle, persist, reset, isRefetching)
|
|
367
|
+
}, [snapshot, loadState, isPersisting, id, handle, persist, reset, isRefetching])
|
|
341
368
|
|
|
342
369
|
// Proxy-based result satisfies the full UseEntityResult<T> at runtime via field access delegation
|
|
343
370
|
return result as UseEntityResult<any, any>
|
|
@@ -351,10 +378,12 @@ function createReadyResult(
|
|
|
351
378
|
handle: EntityAccessor<object, object>,
|
|
352
379
|
persist: () => Promise<void>,
|
|
353
380
|
reset: () => void,
|
|
381
|
+
isRefetching: boolean,
|
|
354
382
|
): UseEntityResult<object, object> {
|
|
355
383
|
const meta: Record<string, unknown> = {
|
|
356
384
|
$status: 'ready' as const,
|
|
357
385
|
$isLoading: false as const,
|
|
386
|
+
$isRefetching: isRefetching,
|
|
358
387
|
$isError: false as const,
|
|
359
388
|
$isNotFound: false as const,
|
|
360
389
|
$error: null,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef, useEffect, useMemo, useCallback } from 'react'
|
|
2
2
|
import type { EntityDef, EntityAccessor, SelectionInput, SelectionMeta, FieldError, SchemaRegistry, CommonEntity, EntityForRoles, RoleNames } from '@contember/bindx'
|
|
3
|
-
import { EntityHandle, isTempId, resolveSelectionMeta, buildQueryFromSelection,
|
|
3
|
+
import { EntityHandle, isTempId, resolveSelectionMeta, buildQueryFromSelection, refreshServerData, createLoadError } from '@contember/bindx'
|
|
4
4
|
import { useBindxContext, useSchemaRegistry } from './BackendAdapterContext.js'
|
|
5
5
|
import { useStoreSubscription } from './useStoreSubscription.js'
|
|
6
6
|
|
|
@@ -43,6 +43,7 @@ interface EntityListResultBase {
|
|
|
43
43
|
export type LoadingEntityListResult = EntityListResultBase & {
|
|
44
44
|
readonly $status: 'loading'
|
|
45
45
|
readonly $isLoading: true
|
|
46
|
+
readonly $isRefetching: false
|
|
46
47
|
readonly $isError: false
|
|
47
48
|
readonly $error: null
|
|
48
49
|
}
|
|
@@ -50,13 +51,21 @@ export type LoadingEntityListResult = EntityListResultBase & {
|
|
|
50
51
|
export type ErrorEntityListResult = EntityListResultBase & {
|
|
51
52
|
readonly $status: 'error'
|
|
52
53
|
readonly $isLoading: false
|
|
54
|
+
readonly $isRefetching: false
|
|
53
55
|
readonly $isError: true
|
|
54
56
|
readonly $error: FieldError
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* `$isRefetching` is `true` while a background re-fetch is in flight
|
|
61
|
+
* (triggered by a `queryKey` change while ready data is already present).
|
|
62
|
+
* The accessor identity stays stable so the subtree does not unmount —
|
|
63
|
+
* stale-while-revalidate semantics.
|
|
64
|
+
*/
|
|
57
65
|
export type ReadyEntityListResult<T extends object> = EntityListResultBase & {
|
|
58
66
|
readonly $status: 'ready'
|
|
59
67
|
readonly $isLoading: false
|
|
68
|
+
readonly $isRefetching: boolean
|
|
60
69
|
readonly $isError: false
|
|
61
70
|
readonly $error: null
|
|
62
71
|
readonly $isDirty: boolean
|
|
@@ -77,6 +86,7 @@ function createLoadingListResult(): LoadingEntityListResult {
|
|
|
77
86
|
return {
|
|
78
87
|
$status: 'loading',
|
|
79
88
|
$isLoading: true,
|
|
89
|
+
$isRefetching: false,
|
|
80
90
|
$isError: false,
|
|
81
91
|
$error: null,
|
|
82
92
|
$add() { throw new Error('Cannot add items while loading') },
|
|
@@ -89,6 +99,7 @@ function createErrorListResult(error: FieldError): ErrorEntityListResult {
|
|
|
89
99
|
return {
|
|
90
100
|
$status: 'error',
|
|
91
101
|
$isLoading: false,
|
|
102
|
+
$isRefetching: false,
|
|
92
103
|
$isError: true,
|
|
93
104
|
$error: error,
|
|
94
105
|
$add() { throw new Error('Cannot add items after error') },
|
|
@@ -199,9 +210,11 @@ export function useEntityList(
|
|
|
199
210
|
status: 'loading' | 'error' | 'ready'
|
|
200
211
|
items: Array<{ id: string; data: object }>
|
|
201
212
|
error?: FieldError
|
|
213
|
+
isRefetching: boolean
|
|
202
214
|
}>({
|
|
203
215
|
status: 'loading',
|
|
204
216
|
items: [],
|
|
217
|
+
isRefetching: false,
|
|
205
218
|
})
|
|
206
219
|
|
|
207
220
|
const versionRef = useRef(0)
|
|
@@ -210,6 +223,7 @@ export function useEntityList(
|
|
|
210
223
|
version: number
|
|
211
224
|
storeVersion: number
|
|
212
225
|
status: string
|
|
226
|
+
isRefetching: boolean
|
|
213
227
|
result: UseEntityListResult<any>
|
|
214
228
|
} | null>(null)
|
|
215
229
|
|
|
@@ -276,7 +290,13 @@ export function useEntityList(
|
|
|
276
290
|
const storeVersion = store.getVersion()
|
|
277
291
|
|
|
278
292
|
const cache = listCacheRef.current
|
|
279
|
-
if (
|
|
293
|
+
if (
|
|
294
|
+
cache &&
|
|
295
|
+
cache.version === version &&
|
|
296
|
+
cache.storeVersion === storeVersion &&
|
|
297
|
+
cache.status === state.status &&
|
|
298
|
+
cache.isRefetching === state.isRefetching
|
|
299
|
+
) {
|
|
280
300
|
return cache.result
|
|
281
301
|
}
|
|
282
302
|
|
|
@@ -300,6 +320,7 @@ export function useEntityList(
|
|
|
300
320
|
result = {
|
|
301
321
|
$status: 'ready',
|
|
302
322
|
$isLoading: false,
|
|
323
|
+
$isRefetching: state.isRefetching,
|
|
303
324
|
$isError: false,
|
|
304
325
|
$error: null,
|
|
305
326
|
$isDirty: false,
|
|
@@ -315,6 +336,7 @@ export function useEntityList(
|
|
|
315
336
|
version,
|
|
316
337
|
storeVersion,
|
|
317
338
|
status: state.status,
|
|
339
|
+
isRefetching: state.isRefetching,
|
|
318
340
|
result,
|
|
319
341
|
}
|
|
320
342
|
|
|
@@ -338,8 +360,16 @@ export function useEntityList(
|
|
|
338
360
|
useEffect(() => {
|
|
339
361
|
const abortController = new AbortController()
|
|
340
362
|
|
|
341
|
-
|
|
363
|
+
// Stale-while-revalidate: if we already have ready items, keep them visible
|
|
364
|
+
// and only flag a background refetch. Otherwise, show explicit loading.
|
|
365
|
+
const prev = listStateRef.current
|
|
366
|
+
if (prev.status === 'ready') {
|
|
367
|
+
listStateRef.current = { ...prev, isRefetching: true }
|
|
368
|
+
} else {
|
|
369
|
+
listStateRef.current = { status: 'loading', items: [], isRefetching: false }
|
|
370
|
+
}
|
|
342
371
|
versionRef.current++
|
|
372
|
+
store.notify()
|
|
343
373
|
|
|
344
374
|
const fetchData = async (): Promise<void> => {
|
|
345
375
|
try {
|
|
@@ -371,13 +401,15 @@ export function useEntityList(
|
|
|
371
401
|
|
|
372
402
|
const items = result.data.map((data: Record<string, unknown>) => {
|
|
373
403
|
const id = data['id'] as string
|
|
404
|
+
// Revalidation: advance the server baseline but keep local dirty
|
|
405
|
+
// edits intact (see EntitySnapshotStore.refreshServerData).
|
|
374
406
|
dispatcher.dispatch(
|
|
375
|
-
|
|
407
|
+
refreshServerData(entityType, id, data),
|
|
376
408
|
)
|
|
377
409
|
return { id, data: data as object }
|
|
378
410
|
})
|
|
379
411
|
|
|
380
|
-
listStateRef.current = { status: 'ready', items }
|
|
412
|
+
listStateRef.current = { status: 'ready', items, isRefetching: false }
|
|
381
413
|
versionRef.current++
|
|
382
414
|
store.notify()
|
|
383
415
|
} catch (error) {
|
|
@@ -388,6 +420,7 @@ export function useEntityList(
|
|
|
388
420
|
status: 'error',
|
|
389
421
|
items: [],
|
|
390
422
|
error: createLoadError(normalizedError),
|
|
423
|
+
isRefetching: false,
|
|
391
424
|
}
|
|
392
425
|
versionRef.current++
|
|
393
426
|
store.notify()
|
|
@@ -27,6 +27,13 @@ interface EntityBaseProps<TRoleMap extends Record<string, object>> {
|
|
|
27
27
|
interface EntityByProps<TRoleMap extends Record<string, object>> extends EntityBaseProps<TRoleMap> {
|
|
28
28
|
/** Unique field(s) to identify the entity (e.g., { id: '...' } or { slug: '...' }) */
|
|
29
29
|
by: EntityUniqueWhere
|
|
30
|
+
/**
|
|
31
|
+
* Cache-invalidation key. Changing this value re-fetches the entity from the
|
|
32
|
+
* server without unmounting the subtree (stale-while-revalidate). Use this
|
|
33
|
+
* instead of `key={...}` when external mutations (e.g. workflow RPC) need
|
|
34
|
+
* the bindx view refreshed but UI state (scroll, dialogs, drafts) must survive.
|
|
35
|
+
*/
|
|
36
|
+
queryKey?: string
|
|
30
37
|
create?: never
|
|
31
38
|
onPersisted?: never
|
|
32
39
|
/** Loading fallback */
|
|
@@ -60,6 +67,7 @@ export type EntityProps<TRoleMap extends Record<string, object> = Record<string,
|
|
|
60
67
|
interface EntityByModeProps {
|
|
61
68
|
entityType: string
|
|
62
69
|
by: EntityUniqueWhere
|
|
70
|
+
queryKey?: string
|
|
63
71
|
children: (entity: EntityAccessor<unknown>) => React.ReactNode
|
|
64
72
|
loading?: React.ReactNode
|
|
65
73
|
error?: (error: FieldError) => React.ReactNode
|
|
@@ -81,6 +89,7 @@ interface EntityCreateModeProps {
|
|
|
81
89
|
function EntityByMode({
|
|
82
90
|
entityType,
|
|
83
91
|
by,
|
|
92
|
+
queryKey: userQueryKey,
|
|
84
93
|
children,
|
|
85
94
|
loading,
|
|
86
95
|
error: errorFallback,
|
|
@@ -93,17 +102,23 @@ function EntityByMode({
|
|
|
93
102
|
const byKey = useMemo(() => JSON.stringify(by), [by])
|
|
94
103
|
|
|
95
104
|
// Phase 1: Collect JSX selection
|
|
96
|
-
const { selection, queryKey } = useSelectionCollection({
|
|
105
|
+
const { selection, queryKey: selectionQueryKey } = useSelectionCollection({
|
|
97
106
|
entityType,
|
|
98
107
|
depsKey: byKey,
|
|
99
108
|
collect: collector => children(collector as EntityAccessor<unknown>),
|
|
100
109
|
})
|
|
101
110
|
|
|
111
|
+
// Combine internal selection key with user-supplied invalidation key so
|
|
112
|
+
// both selection changes and explicit refreshes trigger a refetch.
|
|
113
|
+
const effectiveQueryKey = userQueryKey !== undefined
|
|
114
|
+
? `${selectionQueryKey}::${userQueryKey}`
|
|
115
|
+
: selectionQueryKey
|
|
116
|
+
|
|
102
117
|
// Phase 2: Load data using unified hook
|
|
103
118
|
const result = useEntity({ $name: entityType } as EntityDef, {
|
|
104
119
|
by,
|
|
105
120
|
selection,
|
|
106
|
-
queryKey,
|
|
121
|
+
queryKey: effectiveQueryKey,
|
|
107
122
|
})
|
|
108
123
|
|
|
109
124
|
// Render based on status
|
|
@@ -347,6 +362,7 @@ function EntityImpl<TRoleMap extends Record<string, object>>(
|
|
|
347
362
|
<EntityByMode
|
|
348
363
|
entityType={byProps.entity.$name}
|
|
349
364
|
by={byProps.by}
|
|
365
|
+
queryKey={byProps.queryKey}
|
|
350
366
|
children={byProps.children as (entity: EntityAccessor<unknown>) => React.ReactNode}
|
|
351
367
|
loading={byProps.loading}
|
|
352
368
|
error={byProps.error}
|
|
@@ -19,6 +19,12 @@ export interface EntityListProps<TRoleMap extends Record<string, object> = Recor
|
|
|
19
19
|
limit?: number
|
|
20
20
|
/** Optional offset */
|
|
21
21
|
offset?: number
|
|
22
|
+
/**
|
|
23
|
+
* Cache-invalidation key. Changing this value re-fetches the list without
|
|
24
|
+
* unmounting the subtree (stale-while-revalidate). Use this when external
|
|
25
|
+
* mutations need the bindx view refreshed but UI state must survive.
|
|
26
|
+
*/
|
|
27
|
+
queryKey?: string
|
|
22
28
|
/** Render function receiving typed entity accessor with direct field access */
|
|
23
29
|
children: (entity: EntityAccessor<CommonEntity<TRoleMap>>, index: number) => React.ReactNode
|
|
24
30
|
/** Loading fallback */
|
|
@@ -53,6 +59,7 @@ function EntityListComponent<TRoleMap extends Record<string, object>>({
|
|
|
53
59
|
orderBy,
|
|
54
60
|
limit,
|
|
55
61
|
offset,
|
|
62
|
+
queryKey: userQueryKey,
|
|
56
63
|
children,
|
|
57
64
|
loading,
|
|
58
65
|
error: errorFallback,
|
|
@@ -69,13 +76,18 @@ function EntityListComponent<TRoleMap extends Record<string, object>>({
|
|
|
69
76
|
})
|
|
70
77
|
|
|
71
78
|
// Phase 1: Collect JSX selection
|
|
72
|
-
const { selection, queryKey } = useSelectionCollection({
|
|
79
|
+
const { selection, queryKey: selectionQueryKey } = useSelectionCollection({
|
|
73
80
|
entityType,
|
|
74
81
|
depsKey: optionsKey,
|
|
75
82
|
collect: collector => children(collector as unknown as EntityAccessor<CommonEntity<TRoleMap>>, 0),
|
|
76
83
|
queryKeyExtra: { filter, orderBy, limit, offset },
|
|
77
84
|
})
|
|
78
85
|
|
|
86
|
+
// Combine internal selection key with user-supplied invalidation key
|
|
87
|
+
const effectiveQueryKey = userQueryKey !== undefined
|
|
88
|
+
? `${selectionQueryKey}::${userQueryKey}`
|
|
89
|
+
: selectionQueryKey
|
|
90
|
+
|
|
79
91
|
// Phase 2: Load data using unified hook
|
|
80
92
|
const result = useEntityList(entity, {
|
|
81
93
|
filter,
|
|
@@ -83,7 +95,7 @@ function EntityListComponent<TRoleMap extends Record<string, object>>({
|
|
|
83
95
|
limit,
|
|
84
96
|
offset,
|
|
85
97
|
selection,
|
|
86
|
-
queryKey,
|
|
98
|
+
queryKey: effectiveQueryKey,
|
|
87
99
|
})
|
|
88
100
|
|
|
89
101
|
// Render based on status
|