@rebasepro/app 0.24.0 → 0.24.1-canary.gcb918fc

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.
Files changed (39) hide show
  1. package/README.md +2 -2
  2. package/dist/components/common/useColumnsIds.d.ts +2 -2
  3. package/dist/components/common/useDataTableController.d.ts +15 -1
  4. package/dist/components/common/useScrollRestoration.d.ts +16 -7
  5. package/dist/contexts/CollectionResolverContext.d.ts +8 -1
  6. package/dist/contexts/StorageSourcesContext.d.ts +6 -0
  7. package/dist/core/RebaseProps.d.ts +21 -0
  8. package/dist/core/deprecated_plugin_keys.d.ts +10 -0
  9. package/dist/de-BcBo287O.js +1434 -0
  10. package/dist/de-BcBo287O.js.map +1 -0
  11. package/dist/debug/ui-reference/ComponentSections.d.ts +16 -0
  12. package/dist/debug.js +867 -484
  13. package/dist/es-D-JXBvsW.js +1442 -0
  14. package/dist/es-D-JXBvsW.js.map +1 -0
  15. package/dist/fr-BKGBSaZM.js +1434 -0
  16. package/dist/fr-BKGBSaZM.js.map +1 -0
  17. package/dist/hi-D1jEYlQ1.js +1434 -0
  18. package/dist/hi-D1jEYlQ1.js.map +1 -0
  19. package/dist/hooks/data/collectionWindow.d.ts +146 -13
  20. package/dist/hooks/data/save.d.ts +11 -2
  21. package/dist/hooks/data/useBatchedFetch.d.ts +21 -0
  22. package/dist/hooks/data/useDataOrder.d.ts +6 -0
  23. package/dist/hooks/data/useFetch.d.ts +12 -4
  24. package/dist/hooks/index.d.ts +1 -1
  25. package/dist/hooks/useBuildModeController.d.ts +22 -2
  26. package/dist/hooks/useComponentOverride.d.ts +10 -7
  27. package/dist/hooks/useNavigationBlocker.d.ts +17 -9
  28. package/dist/index.d.ts +0 -1
  29. package/dist/index.es.js +1558 -8659
  30. package/dist/index.es.js.map +1 -1
  31. package/dist/it-DLXLsP1j.js +1434 -0
  32. package/dist/it-DLXLsP1j.js.map +1 -0
  33. package/dist/pt-Dd3JWvUZ.js +1439 -0
  34. package/dist/pt-Dd3JWvUZ.js.map +1 -0
  35. package/dist/util/previews.d.ts +22 -5
  36. package/dist/util/useStorageUploadController.d.ts +6 -2
  37. package/dist/vitePlugin.js +2 -0
  38. package/package.json +11 -7
  39. package/dist/hooks/useBackendStorageSource.d.ts +0 -35
@@ -17,6 +17,15 @@ export interface CollectionWindowState<M extends Record<string, unknown> = Recor
17
17
  * flashing a partial list.
18
18
  */
19
19
  rows: Entity<M>[] | undefined;
20
+ /**
21
+ * The same rows by position, with `undefined` where a row exists but has
22
+ * not been read — the rows above a scroll position the window was
23
+ * restored to ({@link CollectionWindowOptions.seed}), until the view
24
+ * scrolls to them. Absent when nothing is missing, which is always the
25
+ * case for a window no view reports a range to: `rows` is then complete
26
+ * from the first row.
27
+ */
28
+ sparse?: (Entity<M> | undefined)[];
20
29
  /** Rows not yet on screen are being read: the first page, or the next one. */
21
30
  loading: boolean;
22
31
  error: Error | undefined;
@@ -47,8 +56,38 @@ export interface CollectionWindowOptions<M extends Record<string, unknown>> {
47
56
  * The subscription still wins whenever it does answer.
48
57
  */
49
58
  firstPaintTimeoutMs?: number;
59
+ /**
60
+ * The quiet time kept between the end of one re-read of the rows past the
61
+ * first page and the start of the next. Defaults to
62
+ * {@link REREAD_INTERVAL_MS}. A push after a quiet spell is re-read at
63
+ * once; pushes inside this window fold into one re-read at its end.
64
+ */
65
+ rereadIntervalMs?: number;
66
+ /**
67
+ * Rows already known, from a view this window replaces (a scroll position
68
+ * kept while a record was open full screen): `rows` sit at `offset` and
69
+ * on. They are shown at once and read again only when a view reports
70
+ * them in range ({@link CollectionWindow.setVisibleRange}); the rows
71
+ * between the first page and `offset` are not read until a view scrolls
72
+ * to them. `target` should cover them.
73
+ */
74
+ seed?: {
75
+ offset: number;
76
+ rows: Entity<M>[];
77
+ };
50
78
  onChange: (state: CollectionWindowState<M>) => void;
51
79
  }
80
+ /**
81
+ * The quiet time between two re-reads of the rows held past the first page.
82
+ *
83
+ * The server pushes at most once per ~300 ms of quiet per subscription, and at
84
+ * least once a second under a steady stream of writes; each push used to start
85
+ * a re-read of every held row past the first page as soon as the last one
86
+ * ended, so a table scrolled to 10,000 rows ran ten 1,000-row reads back to
87
+ * back for as long as the writes went on. This keeps the duty cycle bounded:
88
+ * at most one re-read per re-read time plus this.
89
+ */
90
+ export declare const REREAD_INTERVAL_MS = 2000;
52
91
  /**
53
92
  * A scrolled collection view's rows, read one page at a time.
54
93
  *
@@ -72,14 +111,34 @@ export interface CollectionWindowOptions<M extends Record<string, unknown>> {
72
111
  * view offers can express — a relevance-ordered search has no cursor — and
73
112
  * because the server breaks ties on the id, so pages neither repeat nor skip
74
113
  * rows under a sort with duplicates.
75
- * - **Realtime** covers every row held, not only the first page. The server
76
- * re-runs a subscription on every write to its collection, so each push after
77
- * the first says "something changed". The rows past the first page are then
78
- * read again, in reads of at most `MAX_LIST_LIMIT`, and replaced in one step.
79
- * Pushes that arrive while that re-read runs are folded into one more.
114
+ * - **Realtime.** The server re-runs a subscription on every write to its
115
+ * collection, so each push after the first says "something changed", and
116
+ * every row held past the first page is stale from then on. Which of them
117
+ * are read again depends on whether a view says what it shows:
118
+ * - A view that reports the rows it draws ({@link setVisibleRange}) has the
119
+ * rows on screen read again, plus a page either side — one read, however
120
+ * far it was scrolled. Rows further out stay stale and are read when they
121
+ * come within half a page of the screen, before they are on it.
122
+ * - Without a report, every row held is read again, in reads of at most
123
+ * `MAX_LIST_LIMIT`, and replaced in one step — the only way a view that
124
+ * cannot say what it shows never shows a stale row.
125
+ * Either way a push after a quiet spell is re-read at once; pushes that
126
+ * arrive while a re-read runs, or within `rereadIntervalMs` of the last one
127
+ * ending, fold into one re-read at the end of that interval — at once if the
128
+ * view asks for more rows or scrolls to rows not read since. The first page
129
+ * itself is applied the moment it is pushed, as always.
80
130
  *
81
- * Rows are de-duplicated by id: between a push moving the first page and the
82
- * re-read of the rest, a row can briefly belong to both.
131
+ * Rows are de-duplicated by id, keeping the copy read last: an insert or a
132
+ * delete above shifts every offset by one, so between a push and the re-read
133
+ * of the rows it moved, a row can sit in two places — on the first page and in
134
+ * the stale row below it, or in the rows just re-read and the stale ones next
135
+ * to them.
136
+ *
137
+ * A window can hold **gaps**: a restored scroll position ({@link
138
+ * CollectionWindowOptions.seed}) shows the rows that were on screen without
139
+ * reading every row above them. A gap is read when a view scrolls to it; a
140
+ * window no view reports a range to reads its gaps at once and shows nothing
141
+ * until they are filled, as it would have read them all before.
83
142
  */
84
143
  export declare class CollectionWindow<M extends Record<string, unknown>> {
85
144
  private readonly accessor;
@@ -92,13 +151,26 @@ export declare class CollectionWindow<M extends Record<string, unknown>> {
92
151
  private head;
93
152
  /** The first page came back full, so there may be more past it. */
94
153
  private headFull;
95
- /** Rows past the first page, from offset `pageSize` on. */
96
- private tail;
97
- /** The last read past the first page came back short. */
154
+ /**
155
+ * Rows past the first page: `slots[i]` is the row at offset
156
+ * `pageSize + i`, `undefined` where it has not been read (a gap).
157
+ */
158
+ private slots;
159
+ /** The last read reaching the end of the held rows came back short. */
98
160
  private tailEnded;
99
- /** The first page changed after it first arrived: re-read the rest. */
100
- private stale;
161
+ /** Moves on with every push after the first: rows read before it are stale. */
162
+ private version;
163
+ /**
164
+ * The offsets a view says it draws, end exclusive. `undefined` until one
165
+ * says so, and again when it goes: every held row is then kept current.
166
+ */
167
+ private range;
168
+ /** The view moved to rows not read since: read them without waiting. */
169
+ private rangeMoved;
170
+ /** Offset of each row (or gap) of the last state reported, by position. */
171
+ private emittedOffsets;
101
172
  private pumping;
173
+ private pumpQueued;
102
174
  private loadingMore;
103
175
  private headError;
104
176
  private tailError;
@@ -108,26 +180,87 @@ export declare class CollectionWindow<M extends Record<string, unknown>> {
108
180
  private liveAnswered;
109
181
  private unsubscribe;
110
182
  private firstPaintTimer;
183
+ /** When the last re-read of the rows past the first page finished. */
184
+ private lastRereadEndedAt;
185
+ /** A re-read waiting out {@link REREAD_INTERVAL_MS}. */
186
+ private rereadTimer;
111
187
  constructor(options: CollectionWindowOptions<M>);
112
188
  start(): void;
113
189
  /** Hold this many rows. Fewer than are held drops the rows past it. */
114
190
  setTarget(target: number): void;
191
+ /**
192
+ * The rows a view draws, by position in the last `rows` (or `sparse`) it
193
+ * was given, end exclusive; `undefined` when it stops drawing them. Rows
194
+ * near the range are kept current and gaps near it are read; see the
195
+ * class comment.
196
+ */
197
+ setVisibleRange(range: {
198
+ start: number;
199
+ end: number;
200
+ } | undefined): void;
201
+ /**
202
+ * What to keep of this window to show it again later: the rows read
203
+ * around the range a view last reported — or, with none reported, every
204
+ * row from the first — the offset of the first of them, and how many
205
+ * rows the window holds in all. See {@link CollectionWindowOptions.seed}.
206
+ */
207
+ snapshot(): {
208
+ offset: number;
209
+ rows: Entity<M>[];
210
+ held: number;
211
+ };
115
212
  dispose(): void;
116
213
  private readHeadOnce;
117
214
  private receiveLiveError;
118
215
  private receiveHead;
119
216
  /** Rows still wanted past the ones held. */
120
217
  private wanted;
218
+ private needsRead;
219
+ /**
220
+ * The held rows to read again now, by slot, or `undefined` if none.
221
+ *
222
+ * With a range: the stale rows and gaps within a page of it, once any of
223
+ * them is within half a page — so scrolling into stale rows reads a
224
+ * stretch of them ahead of the screen rather than one row per frame.
225
+ * Without: every stale row and gap, as one span.
226
+ */
227
+ private nextRefresh;
228
+ private schedulePump;
121
229
  /**
122
230
  * The one loop that reads past the first page, so a re-read and a next page
123
231
  * never run at the same time and interleave their offsets.
124
232
  */
125
233
  private pump;
234
+ /** How long a re-read due now still has to wait. 0 after a quiet spell. */
235
+ private rereadWait;
236
+ private armReread;
126
237
  private readNext;
127
- /** Read every held row past the first page again, and swap them in at once. */
238
+ /**
239
+ * Read the held rows in slots `[from, to)` again, and swap them in at once.
240
+ * A read that comes back short says where the collection ends now: the
241
+ * rows held past that are dropped.
242
+ */
128
243
  private reread;
244
+ /**
245
+ * Every one-shot read the window makes: its query, never counted.
246
+ *
247
+ * Nothing here reads `meta.total` — a page ends the window by coming back
248
+ * short — and the `COUNT(*)` beside each page read every matching row to
249
+ * report a number that was thrown away: scrolling a table to 10,000 rows
250
+ * ran 199 of them. The first page's subscription is the accessor's and is
251
+ * left as it is.
252
+ */
253
+ private oneShot;
129
254
  private process;
130
255
  private complete;
256
+ private hasGaps;
131
257
  private emit;
258
+ /**
259
+ * The rows to show, in order, each id once — the copy read last wins, the
260
+ * first page counting as read now — with gaps kept as positions in
261
+ * `sparse`, and the offset each position stands for.
262
+ */
132
263
  private rows;
264
+ /** Positions in the last state reported, as offsets in the collection. */
265
+ private toOffsets;
133
266
  }
@@ -11,6 +11,13 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, unknown>> = {
11
11
  previousValues?: Partial<EntityValues<M>>;
12
12
  collection?: AdminCollection<M>;
13
13
  status: EntityStatus;
14
+ /**
15
+ * The version the edit was made against — the edited entity's
16
+ * {@link Entity.etag}. An update is then refused with `412
17
+ * PRECONDITION_FAILED` if the record has changed since, rather than
18
+ * writing over the change. Ignored for a create.
19
+ */
20
+ ifMatch?: string;
14
21
  afterSave?: (updatedEntity: Entity<M>) => void;
15
22
  afterSaveError?: (e: Error) => void;
16
23
  };
@@ -33,7 +40,9 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, unknown>> = {
33
40
  *
34
41
  * `afterSave`/`afterSaveError` below are the *caller's* UI callbacks — the
35
42
  * form's "close the dialog", the table's "clear the editing state" — and are
36
- * unrelated to the collection's. Both run: the collection's first.
43
+ * unrelated to the collection's. Both run: the collection's first. A
44
+ * collection `afterSave` that throws does not undo the save: the write has
45
+ * committed, so it is reported as a warning and the caller's `afterSave` runs.
37
46
  *
38
47
  * @param collection
39
48
  * @param path
@@ -47,7 +56,7 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, unknown>> = {
47
56
  * @param afterSaveError
48
57
  * @group Hooks and utilities
49
58
  */
50
- export declare function saveEntityWithCallbacks<M extends Record<string, unknown>>({ collection, path, entityId, values, previousValues, status, data, context, afterSave, afterSaveError }: Omit<SaveEntityWithCallbacksProps<M>, "collection"> & {
59
+ export declare function saveEntityWithCallbacks<M extends Record<string, unknown>>({ collection, path, entityId, values, previousValues, status, ifMatch, data, context, afterSave, afterSaveError }: Omit<SaveEntityWithCallbacksProps<M>, "collection"> & {
51
60
  collection: AdminCollection<M>;
52
61
  data: RebaseData;
53
62
  context: RebaseContext;
@@ -0,0 +1,21 @@
1
+ import type { AdminCollection } from "@rebasepro/cms-types";
2
+ import type { FetchResult } from "./useFetch.js";
3
+ /**
4
+ * {@link useFetch} for a record many previews on one screen ask for at once —
5
+ * the target of a reference in a table cell, a card, an array of references.
6
+ *
7
+ * The ids every preview asks for in one tick are read together: one query and
8
+ * one live subscription per batch, not one `listenById` per record. Updates
9
+ * reach a preview per batch — a write to any record of the batch refetches the
10
+ * batch — which is what a preview needs.
11
+ *
12
+ * For one record a view edits, use {@link useFetch}: it follows that record
13
+ * alone and seeds from the record cache.
14
+ *
15
+ * @group Hooks and utilities
16
+ */
17
+ export declare function useBatchedFetch<M extends Record<string, any>>({ path, entityId, collection }: {
18
+ path: string;
19
+ entityId?: string | number;
20
+ collection: AdminCollection<M>;
21
+ }): FetchResult<M>;
@@ -5,6 +5,12 @@ export interface DataOrderProps<M extends Record<string, any>> {
5
5
  }
6
6
  /**
7
7
  * This hook is used to have some entities at the beginning of data.
8
+ *
9
+ * The result is the same array until `data` or `entitiesDisplayedFirst`
10
+ * changes. It used to be rebuilt on every call whenever rows were pinned, so
11
+ * the table controller around it — and everything rendering from that — was
12
+ * new on every render.
13
+ *
8
14
  * @param path
9
15
  * @param entitiesDisplayedFirst
10
16
  * @group Hooks and utilities
@@ -9,6 +9,14 @@ export interface FetchProps<M extends Record<string, any>, USER extends User = U
9
9
  databaseId?: string;
10
10
  collection: AdminCollection<M, USER>;
11
11
  useCache?: boolean;
12
+ /**
13
+ * Load the collection's relation properties into the record, as a list
14
+ * read loads them into each row. The record views ask for it: a form built
15
+ * on a record without them shows a many-to-many field empty, and saving it
16
+ * sets that empty membership. A preview leaves it off, since it would load
17
+ * every related row of the record it previews.
18
+ */
19
+ relations?: boolean;
12
20
  }
13
21
  /**
14
22
  * @group Hooks and utilities
@@ -19,9 +27,9 @@ export interface FetchResult<M extends Record<string, any>> {
19
27
  dataLoadingError?: Error;
20
28
  }
21
29
  /**
22
- * Pre-populate the entity fetch cache with entities loaded from a collection.
23
- * This allows entity detail views to render instantly using cached data,
24
- * while the background fetch/listener brings in fresh data.
30
+ * Put records in the entity fetch cache, so a view that opens one renders it at
31
+ * once while the background fetch/listener brings in fresh data. The cache
32
+ * keeps the 2,000 records written or read most recently.
25
33
  * @param path - The collection path (e.g. "products")
26
34
  * @param entities - Array of entities to cache
27
35
  */
@@ -40,4 +48,4 @@ export declare function clearFetchCache(): void;
40
48
  * @param useCache
41
49
  * @group Hooks and utilities
42
50
  */
43
- export declare function useFetch<M extends Record<string, any>, USER extends User = User>({ path, entityId, collection, databaseId, useCache }: FetchProps<M, USER>): FetchResult<M>;
51
+ export declare function useFetch<M extends Record<string, any>, USER extends User = User>({ path, entityId, collection, databaseId, useCache, relations }: FetchProps<M, USER>): FetchResult<M>;
@@ -2,6 +2,7 @@ export * from "./data/useData.js";
2
2
  export * from "./data/useCollection.js";
3
3
  export * from "./data/collectionWindow.js";
4
4
  export * from "./data/useFetch.js";
5
+ export * from "./data/useBatchedFetch.js";
5
6
  export * from "./data/useRelationSelector.js";
6
7
  export * from "./data/save.js";
7
8
  export * from "./data/delete.js";
@@ -26,7 +27,6 @@ export * from "./useCustomizationController.js";
26
27
  export * from "./useBuildLocalConfigurationPersistence.js";
27
28
  export * from "./useBuildModeController.js";
28
29
  export * from "./useRebaseRegistry.js";
29
- export * from "./useBackendStorageSource.js";
30
30
  export * from "./usePermissions.js";
31
31
  export * from "./ApiConfigContext.js";
32
32
  export * from "./useTranslation.js";
@@ -1,6 +1,26 @@
1
1
  import type { ModeController } from "./useModeController.js";
2
+ /**
3
+ * @group Hooks and utilities
4
+ */
5
+ export interface BuildModeControllerOptions {
6
+ /**
7
+ * `false` builds a controller that touches nothing outside React: no `dark`
8
+ * class on `<html>`, no `prefers-color-scheme` listener, no localStorage.
9
+ * `<Rebase>` passes it when the host app hands over its own
10
+ * `modeController`. Defaults to `true`.
11
+ */
12
+ enabled?: boolean;
13
+ }
2
14
  /**
3
15
  * Use this hook to build a color mode controller that determines
4
- * the theme of the admin
16
+ * the theme of the admin.
17
+ *
18
+ * While enabled it owns the page's theme: it puts the `dark` class on
19
+ * `document.documentElement`, follows `prefers-color-scheme` until the user
20
+ * picks a mode, and remembers that pick in localStorage. When the component
21
+ * using it unmounts, it takes away the `dark` class if it was the one that
22
+ * added it.
23
+ *
24
+ * @group Hooks and utilities
5
25
  */
6
- export declare function useBuildModeController(): ModeController;
26
+ export declare function useBuildModeController(options?: BuildModeControllerOptions): ModeController;
@@ -3,15 +3,18 @@ import type { OverridableComponentName } from "@rebasepro/cms-types";
3
3
  /**
4
4
  * Resolves a potentially overridden component.
5
5
  *
6
- * Resolution order:
7
- * 1. Collection-scoped override (highest priority)
8
- * 2. Global override
9
- * 3. Default component (fallback)
6
+ * The two scopes compose, innermost first:
7
+ * 1. Default component
8
+ * 2. Global override (`<Rebase components>`), applied to the default
9
+ * 3. Collection-scoped override (`admin.components`), applied to the result
10
+ * of 2
10
11
  *
11
12
  * Supports two override modes:
12
- * - **Eject** (default): The override component fully replaces the default.
13
- * - **Wrap** (`wrap: true`): The override component wraps the default.
14
- * The default is passed as `OriginalComponent` in props.
13
+ * - **Eject** (default): The override component fully replaces what it is
14
+ * applied to.
15
+ * - **Wrap** (`wrap: true`): The override component wraps what it is applied
16
+ * to, passed as `OriginalComponent` in props — so a collection's wrapper
17
+ * wraps the global override when there is one, not the built-in default.
15
18
  *
16
19
  * @param name - The overridable component name (e.g. `"Entity.Form"`)
17
20
  * @param DefaultComponent - The built-in default component
@@ -1,17 +1,25 @@
1
1
  import React from "react";
2
2
  import { Blocker, BlockerFunction } from "react-router";
3
3
  /**
4
- * Owns the single React Router blocker for the whole app.
4
+ * Owns the single navigation guard for the whole app.
5
5
  *
6
- * React Router only honours **one** blocker at a time: `shouldBlockNavigation`
7
- * picks the last-registered blocker function and silently ignores every other
8
- * one (it only logs a dev warning). Because blockers register in a `useEffect`,
9
- * the winner is whichever surface mounted most recently — so an unsaved-changes
10
- * guard could be disabled by an unrelated component mounting after it.
6
+ * Under a React Router **data router** (`createBrowserRouter` +
7
+ * `<RouterProvider>`) it holds the one `useBlocker` in the tree. React Router
8
+ * only honours one blocker at a time: `shouldBlockNavigation` picks the
9
+ * last-registered blocker function and silently ignores every other one (it
10
+ * only logs a dev warning). Because blockers register in a `useEffect`, the
11
+ * winner is whichever surface mounted most recently — so an unsaved-changes
12
+ * guard could be disabled by an unrelated component mounting after it. This
13
+ * provider registers the only one and multiplexes it, so every surface that
14
+ * guards navigation gets a say regardless of mount order.
11
15
  *
12
- * This provider registers the only `useBlocker` in the tree and multiplexes it,
13
- * so every surface that needs to guard navigation gets a say regardless of
14
- * mount order. Surfaces register through {@link useNavigationBlocker}.
16
+ * Under any **other** router (a plain `<BrowserRouter>`, a `<MemoryRouter>`)
17
+ * there is no `useBlocker` to call — it throws outside a data router — and no
18
+ * way to intercept an in-app navigation. The guards then degrade to the
19
+ * browser's own: leaving the page (reload, tab close, typing a URL) asks for
20
+ * confirmation while any registered guard would block leaving.
21
+ *
22
+ * Surfaces register through {@link useNavigationBlocker}.
15
23
  */
16
24
  export declare function NavigationBlockerProvider({ children }: {
17
25
  children: React.ReactNode;
package/dist/index.d.ts CHANGED
@@ -13,7 +13,6 @@ export type { RebaseStorageSource } from "./core/RebaseProps.js";
13
13
  export { UnsavedChangesDialog } from "./components/UnsavedChangesDialog.js";
14
14
  export * from "./i18n/RebaseI18nProvider.js";
15
15
  export * from "./locales/en.js";
16
- export * from "./locales/es.js";
17
16
  export * from "./hooks/useStudioBridge.js";
18
17
  export { useBridgeRegistration } from "./hooks/useBridgeRegistration.js";
19
18
  export * from "./collections/index.js";