@rebasepro/app 0.13.0 → 0.13.1-canary.g06dbe5b

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 (50) hide show
  1. package/dist/collections/entity-display-cache.d.ts +75 -0
  2. package/dist/collections/entity-display.d.ts +35 -0
  3. package/dist/collections/entity_image_preview.d.ts +8 -0
  4. package/dist/collections/form-layout.d.ts +6 -0
  5. package/dist/collections/index.d.ts +4 -0
  6. package/dist/collections/property-path.d.ts +16 -0
  7. package/dist/collections/property_presentation.d.ts +18 -3
  8. package/dist/collections/summary-property.d.ts +65 -0
  9. package/dist/components/LoginView/oauth-redirect-flow.d.ts +86 -0
  10. package/dist/components/common/useColumnsIds.d.ts +22 -0
  11. package/dist/core/Rebase.d.ts +1 -1
  12. package/dist/core/RebaseProps.d.ts +10 -2
  13. package/dist/hooks/data/collectionQuery.d.ts +45 -0
  14. package/dist/hooks/data/useRelationSelector.d.ts +12 -1
  15. package/dist/index.es.js +866 -241
  16. package/dist/index.es.js.map +1 -1
  17. package/dist/util/entity_cache.d.ts +19 -3
  18. package/dist/util/previews.d.ts +19 -0
  19. package/package.json +7 -7
  20. package/src/auth/useRebaseAuthController.ts +15 -1
  21. package/src/collections/entity-display-cache.ts +195 -0
  22. package/src/collections/entity-display.ts +105 -0
  23. package/src/collections/entity_image_preview.ts +13 -0
  24. package/src/collections/form-layout.ts +8 -1
  25. package/src/collections/index.ts +4 -0
  26. package/src/collections/property-path.ts +30 -0
  27. package/src/collections/property_presentation.ts +39 -4
  28. package/src/collections/summary-property.ts +114 -0
  29. package/src/collections/title-property.ts +13 -16
  30. package/src/components/LoginView/LoginView.tsx +48 -26
  31. package/src/components/LoginView/oauth-redirect-flow.ts +184 -0
  32. package/src/components/common/useColumnsIds.tsx +67 -29
  33. package/src/components/common/useDataTableController.tsx +10 -14
  34. package/src/core/Rebase.tsx +20 -2
  35. package/src/core/RebaseProps.tsx +10 -2
  36. package/src/hooks/data/collectionQuery.ts +55 -0
  37. package/src/hooks/data/useCollection.tsx +29 -21
  38. package/src/hooks/data/useFetch.tsx +17 -2
  39. package/src/hooks/data/useRelationSelector.tsx +46 -8
  40. package/src/hooks/useAuthSubscription.ts +18 -2
  41. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +20 -12
  42. package/src/locales/de.ts +2 -0
  43. package/src/locales/en.ts +3 -0
  44. package/src/locales/es.ts +2 -0
  45. package/src/locales/fr.ts +2 -0
  46. package/src/locales/hi.ts +3 -1
  47. package/src/locales/it.ts +2 -0
  48. package/src/locales/pt.ts +2 -0
  49. package/src/util/entity_cache.ts +34 -31
  50. package/src/util/previews.ts +49 -29
@@ -6,7 +6,7 @@ import type { ComponentOverrideMap } from "@rebasepro/admin-types";
6
6
  import React, { useEffect, useMemo, useRef, useState } from "react";
7
7
  import { CenteredView, Typography } from "@rebasepro/ui";
8
8
  import { User, CollectionRegistryController, DataDriver, DataSourceDefinition, RebaseData, DEFAULT_DATA_SOURCE_KEY, StorageSource, StorageSourceDefinition, DEFAULT_STORAGE_SOURCE_KEY } from "@rebasepro/types";
9
- import { RebaseContext } from "@rebasepro/admin-types";
9
+ import { RebaseContext, UNRENDERED_SLOTS } from "@rebasepro/admin-types";
10
10
  import { PluginProviderStack } from "./PluginProviderStack";
11
11
  import { PluginLifecycleManager } from "./PluginLifecycleManager";
12
12
  import { AuthControllerContext, CollectionResolverRegistrationContext, CollectionResolver } from "../contexts";
@@ -46,7 +46,7 @@ import { useBuildEffectiveRoleController } from "../hooks/useBuildEffectiveRoleC
46
46
  *
47
47
  * @group Core
48
48
  */
49
- export function Rebase<USER extends User>(props: RebaseProps<USER>) {
49
+ export function Rebase<USER extends User, DB = unknown>(props: RebaseProps<USER, DB>) {
50
50
 
51
51
  const {
52
52
  children,
@@ -90,6 +90,24 @@ export function Rebase<USER extends User>(props: RebaseProps<USER>) {
90
90
  ...((plugins ?? []).flatMap((p) => p.slots ?? []))
91
91
  ], [directSlots, plugins]);
92
92
 
93
+ // Seven of the twenty-nine declared slots are rendered nowhere. They have
94
+ // props interfaces and rows in the public slot reference, so registering
95
+ // for one looks exactly like registering for one that works — and then
96
+ // nothing happens, with no way to tell whether the fault is the plugin's.
97
+ // `UNRENDERED_SLOTS` is kept honest by `slot-render-sites.test.ts`, which
98
+ // derives the same set by scanning for render sites.
99
+ useEffect(() => {
100
+ const dead = new Set<string>(UNRENDERED_SLOTS);
101
+ const registered = new Set(resolvedSlots.map(s => s.slot).filter(slot => dead.has(slot)));
102
+ for (const slot of registered) {
103
+ console.warn(
104
+ `[Rebase] A component is registered for the "${slot}" slot, which this ` +
105
+ "build declares but renders nowhere — nothing will appear. This is a gap " +
106
+ "in Rebase, not in your plugin."
107
+ );
108
+ }
109
+ }, [resolvedSlots]);
110
+
93
111
 
94
112
  // Auth fallback logic
95
113
  const clientAuthController = useAuthSubscription(authControllerProp ? undefined : client?.auth);
@@ -105,7 +105,7 @@ import type { EffectiveRoleController } from "@rebasepro/types";
105
105
  *
106
106
  * @group Models
107
107
  */
108
- export type RebaseProps<USER extends User> = {
108
+ export type RebaseProps<USER extends User, DB = unknown> = {
109
109
 
110
110
  /**
111
111
  * The root components of your application. Use RebaseAdmin, RebaseStudio, and RebaseShell.
@@ -151,8 +151,16 @@ export type RebaseProps<USER extends User> = {
151
151
  * entry keyed `"(default)"` carries a driver.
152
152
  * `client.auth` is subscribed unless `authController` is provided.
153
153
  * `client.storage` is used unless `storageSource` is provided.
154
+ *
155
+ * `DB` is inferred from whatever is passed, and defaults to `unknown` for
156
+ * an untyped client. It has to be a parameter rather than a fixed
157
+ * `RebaseClient`: `RebaseClient<unknown>` is not a supertype of
158
+ * `RebaseClient<Database>` — the dynamic branch of `RebaseSdkData` is an
159
+ * index signature that no concrete instantiation satisfies — so pinning it
160
+ * here rejected the client of every project that generates a `Database`
161
+ * type, which is the whole typed-SDK path.
154
162
  */
155
- client?: RebaseClient;
163
+ client?: RebaseClient<DB>;
156
164
 
157
165
  /**
158
166
  * The data sources of the app. Each entry pairs a
@@ -0,0 +1,55 @@
1
+ import type { FindParams, FilterValues, LogicalCondition } from "@rebasepro/types";
2
+
3
+ /**
4
+ * The one place a collection read's parameters are assembled.
5
+ *
6
+ * ## Why this exists
7
+ *
8
+ * Four call sites used to build this object by hand — `useCollection` and
9
+ * `useDataTableController`, each in both their `listen` and `find` branches.
10
+ * Every one of them listed the fields it forwarded, so adding an option to
11
+ * `FindParams` meant remembering four places, and forgetting one failed
12
+ * silently: the option simply never reached the server, and the feature it
13
+ * enabled looked broken for reasons nowhere near the code that dropped it.
14
+ *
15
+ * That is not hypothetical. `searchExplain` was added to three of the four and
16
+ * the miss was found by intercepting a WebSocket frame, because nothing else
17
+ * could see it.
18
+ *
19
+ * A field added here reaches every read. `toFindParams` returns `FindParams`
20
+ * itself, so the compiler checks the shape rather than a hand-copied list.
21
+ */
22
+ export interface CollectionQueryInput<M extends Record<string, unknown>> {
23
+ where?: FilterValues<Extract<keyof M, string>>;
24
+ logical?: LogicalCondition;
25
+ /**
26
+ * Loosened from `FindParams["orderBy"]` on purpose: callers hold the sort
27
+ * as a plain `[string, direction]` read off user config or a URL, where the
28
+ * column is not statically known. Narrowing it here would push a cast onto
29
+ * every call site, which is the kind of friction that sends people back to
30
+ * hand-building the object this exists to replace.
31
+ */
32
+ orderBy?: [string, "asc" | "desc"];
33
+ limit?: number;
34
+ offset?: number;
35
+ page?: number;
36
+ searchString?: string;
37
+ include?: string[];
38
+ }
39
+
40
+ /**
41
+ * Build the parameters for one collection read.
42
+ *
43
+ * Derived options live here rather than at the call sites, so they cannot
44
+ * disagree: `searchExplain` rides along exactly when there is a search string
45
+ * to explain, which is also the only time it costs anything.
46
+ */
47
+ export function toFindParams<M extends Record<string, unknown>>(
48
+ input: CollectionQueryInput<M>
49
+ ): FindParams<M> {
50
+ const { searchString, ...rest } = input;
51
+ return {
52
+ ...rest,
53
+ ...(searchString ? { searchString, searchExplain: true } : {})
54
+ } as FindParams<M>;
55
+ }
@@ -3,6 +3,7 @@ import { useEffect, useState, useMemo } from "react";
3
3
  import { Entity, FilterValues, User } from "@rebasepro/types";
4
4
  import { useData } from "./useData";
5
5
  import { isSchemaDriftError, useSchemaDriftContext } from "../../components/SchemaDriftBanner";
6
+ import { toFindParams } from "./collectionQuery";
6
7
  import { getRelationIncludeParams } from "../../util/previews";
7
8
  import type { AdminCollection } from "@rebasepro/admin-types";
8
9
  /**
@@ -136,30 +137,37 @@ export function useCollection<M extends Record<string, any>, USER extends User>(
136
137
  const includeParams = getRelationIncludeParams(collection);
137
138
 
138
139
  if (accessor.listen) {
139
- return accessor.listen({
140
- where: whereParams,
141
- limit: itemCount,
142
- offset,
143
- page,
144
- orderBy: orderByParams,
145
- searchString,
146
- include: includeParams
147
- }, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
140
+ // Assembled in one place — see `toFindParams`.
141
+ return accessor.listen(
142
+ toFindParams({ where: whereParams, limit: itemCount, offset, page,
143
+ orderBy: orderByParams, searchString, include: includeParams }),
144
+ (res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
148
145
  meta: res.meta }), onError);
149
146
  } else {
150
- accessor.find({
151
- where: whereParams,
152
- limit: itemCount,
153
- offset,
154
- page,
155
- orderBy: orderByParams,
156
- searchString,
157
- include: includeParams
158
- })
159
- .then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
160
- meta: res.meta }))
161
- .catch(onError);
147
+ // The one-shot fallback, taken whenever the client has no socket.
148
+ // `listen` above cancels on cleanup, and every dependency of this
149
+ // effect — the search string, the filters, the sort, the page —
150
+ // changes while a request is in flight. A promise cannot be
151
+ // unsubscribed, so the cleanup has to disown its result instead:
152
+ // without this, whichever response arrives last wins, and responses
153
+ // do not arrive in request order. Typing into search would settle
154
+ // on the results for whichever query the server happened to finish
155
+ // last.
156
+ let cancelled = false;
157
+ accessor.find(
158
+ toFindParams({ where: whereParams, limit: itemCount, offset, page,
159
+ orderBy: orderByParams, searchString, include: includeParams }))
160
+ .then((res) => {
161
+ if (cancelled) return;
162
+ onEntitiesUpdate({ data: res.data as Entity<M>[],
163
+ meta: res.meta });
164
+ })
165
+ .catch((e) => {
166
+ if (cancelled) return;
167
+ onError(e);
168
+ });
162
169
  return () => {
170
+ cancelled = true;
163
171
  };
164
172
  }
165
173
  }, [path, itemCount, offset, page, currentSort, sortByProperty, filterValues, searchString, dataClient, collection]);
@@ -117,10 +117,25 @@ export function useFetch<M extends Record<string, any>, USER extends User = User
117
117
  if (accessor.listenById) {
118
118
  return accessor.listenById(entityId, (entity) => onEntityUpdate(entity as Entity<M> | undefined), onError);
119
119
  } else {
120
+ // The one-shot fallback, taken whenever the client has no
121
+ // socket. `listenById` above cancels on cleanup; a promise
122
+ // cannot, so the cleanup disowns its result instead. `entityId`
123
+ // is an effect dependency, and where this hook's caller is not
124
+ // remounted per record — the side panel replaces its contents
125
+ // in place — a slow response for the record just left would
126
+ // otherwise land on top of the one now showing.
127
+ let cancelled = false;
120
128
  accessor.findById(entityId)
121
- .then((entity) => onEntityUpdate(entity as Entity<M> | undefined))
122
- .catch(onError);
129
+ .then((entity) => {
130
+ if (cancelled) return;
131
+ onEntityUpdate(entity as Entity<M> | undefined);
132
+ })
133
+ .catch((e) => {
134
+ if (cancelled) return;
135
+ onError(e);
136
+ });
123
137
  return () => {
138
+ cancelled = true;
124
139
  };
125
140
  }
126
141
  }
@@ -41,6 +41,17 @@ export interface UseRelationSelectorProps<M extends Record<string, any> = any> {
41
41
  * Property name to use as the secondary display field
42
42
  */
43
43
  descriptionProperty?: keyof M;
44
+ /**
45
+ * Whether the list should be fetched at all. Defaults to `true`.
46
+ *
47
+ * A picker that is mounted is not a picker that is open, and the two used
48
+ * to be the same thing here: the fetch ran on mount, so a collection table
49
+ * with a relation column paid for one query — or one realtime subscription
50
+ * — per rendered row before anyone clicked a cell. Pass `false` until the
51
+ * list is actually needed and nothing is requested; flipping it to `true`
52
+ * fetches once, and it never goes back.
53
+ */
54
+ enabled?: boolean;
44
55
  }
45
56
 
46
57
  export interface RelationSelectorController {
@@ -66,7 +77,8 @@ export function useRelationSelector<M extends Record<string, any> = any>(
66
77
  pageSize = DEFAULT_PAGE_SIZE,
67
78
  getLabelFromEntity,
68
79
  getDescriptionFromEntity,
69
- descriptionProperty
80
+ descriptionProperty,
81
+ enabled = true
70
82
  }: UseRelationSelectorProps<M>
71
83
  ): RelationSelectorController {
72
84
 
@@ -82,6 +94,10 @@ export function useRelationSelector<M extends Record<string, any> = any>(
82
94
 
83
95
  const unsubscribeRef = useRef<(() => void) | null>(null);
84
96
  const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
97
+ // Whether a fetch has ever completed. Between `enabled` flipping and the
98
+ // effect running there is one paint with no items and no request in
99
+ // flight; without this the list shows "no results" for that frame.
100
+ const hasLoadedRef = useRef(false);
85
101
 
86
102
  const setLoading = useCallback((loading: boolean) => {
87
103
  isLoadingRef.current = loading;
@@ -149,6 +165,7 @@ export function useRelationSelector<M extends Record<string, any> = any>(
149
165
 
150
166
  const onEntitiesUpdate = (res: { data: Entity<M>[], meta: { hasMore: boolean } }) => {
151
167
  const newItems = res.data.map((e) => entityToRelationItem(e));
168
+ hasLoadedRef.current = true;
152
169
  setItems(newItems);
153
170
  setHasMore(res.meta.hasMore);
154
171
  setLoading(false);
@@ -156,6 +173,7 @@ export function useRelationSelector<M extends Record<string, any> = any>(
156
173
 
157
174
  const onErrorUpdate = (fetchError: Error) => {
158
175
  console.error("useRelationSelector: Error fetching data:", fetchError);
176
+ hasLoadedRef.current = true;
159
177
  setError(fetchError);
160
178
  setLoading(false);
161
179
  };
@@ -174,6 +192,14 @@ export function useRelationSelector<M extends Record<string, any> = any>(
174
192
  }, (res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
175
193
  meta: res.meta }), onErrorUpdate);
176
194
  } else {
195
+ // The one-shot fallback, taken whenever the client has no socket.
196
+ // `cleanupSubscription` is what keeps this hook's results matching
197
+ // the query that asked for them, and it can only do that if the
198
+ // fallback hands back something that actually cancels. A promise
199
+ // cannot be unsubscribed, so this disowns its result instead —
200
+ // otherwise the debounce is the only thing between a slow response
201
+ // for "ab" and its landing on top of the results for "abc".
202
+ let cancelled = false;
177
203
  accessor.find({
178
204
  where: whereParams,
179
205
  limit: limit,
@@ -182,10 +208,18 @@ meta: res.meta }), onErrorUpdate);
182
208
  searchString: currentSearch,
183
209
  include: includeParams
184
210
  })
185
- .then((res) => onEntitiesUpdate({ data: res.data as Entity<M>[],
186
- meta: res.meta }))
187
- .catch(onErrorUpdate);
188
- unsubscribe = () => {};
211
+ .then((res) => {
212
+ if (cancelled) return;
213
+ onEntitiesUpdate({ data: res.data as Entity<M>[],
214
+ meta: res.meta });
215
+ })
216
+ .catch((e) => {
217
+ if (cancelled) return;
218
+ onErrorUpdate(e);
219
+ });
220
+ unsubscribe = () => {
221
+ cancelled = true;
222
+ };
189
223
  }
190
224
 
191
225
  unsubscribeRef.current = unsubscribe || null;
@@ -215,12 +249,13 @@ meta: res.meta }))
215
249
 
216
250
  // Load initial data and update upon changes
217
251
  useEffect(() => {
252
+ if (!enabled) return;
218
253
  fetchData();
219
254
 
220
255
  return () => {
221
256
  cleanupSubscription();
222
257
  };
223
- }, [fetchData]);
258
+ }, [fetchData, enabled, cleanupSubscription]);
224
259
 
225
260
  useEffect(() => {
226
261
  return () => {
@@ -232,11 +267,14 @@ meta: res.meta }))
232
267
 
233
268
  return useMemo(() => ({
234
269
  items,
235
- isLoading,
270
+ // Enabled but not answered yet still counts as loading, so the frame
271
+ // between opening the picker and the request starting shows a spinner
272
+ // rather than "no results".
273
+ isLoading: isLoading || (enabled && !hasLoadedRef.current),
236
274
  error,
237
275
  search,
238
276
  loadMore,
239
277
  hasMore,
240
278
  entityToRelationItem
241
- }), [items, isLoading, error, search, loadMore, hasMore, entityToRelationItem]);
279
+ }), [items, isLoading, error, search, loadMore, hasMore, entityToRelationItem, enabled]);
242
280
  }
@@ -50,9 +50,25 @@ export function useAuthSubscription(authClient?: AuthClient): AuthController {
50
50
  if (user) {
51
51
  setUser(user);
52
52
  }
53
+ setAuthError(undefined);
53
54
  })
54
- .catch(e => {
55
- // Ignore, user just isn't logged in
55
+ .catch((e: unknown) => {
56
+ // A 401 or 403 is the ordinary first load of an anonymous
57
+ // visitor: there is no session, which is not an error.
58
+ //
59
+ // Everything else is. This used to swallow the lot with
60
+ // "Ignore, user just isn't logged in", leaving `authError`
61
+ // — a field this controller declares and `Rebase` renders a
62
+ // full-screen "Error loading auth" view for — permanently
63
+ // undefined. So the view was unreachable, and a backend that
64
+ // was down, misconfigured, or blocked by CORS looked exactly
65
+ // like being signed out: the app showed the login form.
66
+ const status = (e as { status?: number } | undefined)?.status;
67
+ if (status === 401 || status === 403) {
68
+ setAuthError(undefined);
69
+ return;
70
+ }
71
+ setAuthError(e instanceof Error ? e : new Error(String(e)));
56
72
  })
57
73
  .finally(() => {
58
74
  setInitialLoading(false);
@@ -1,15 +1,16 @@
1
1
  import { useCallback, useEffect, useRef, useState, useMemo } from "react";
2
2
  import { PartialCollectionConfig, UserConfigurationPersistence } from "@rebasepro/admin-types";
3
3
  import { stripCollectionPath } from "@rebasepro/common";
4
- import { mergeDeep } from "@rebasepro/utils";
4
+ import { isArrayValue, isRecordValue, mergeDeep, readStoredJson, writeStoredJson } from "@rebasepro/utils";
5
5
 
6
6
  export function useBuildLocalConfigurationPersistence(): UserConfigurationPersistence {
7
7
 
8
8
  const configCache = useRef<Record<string, PartialCollectionConfig>>({});
9
9
 
10
- const getCollectionFromStorage = useCallback((storageKey: string) => {
11
- const item = localStorage.getItem(storageKey);
12
- return item ? JSON.parse(item) : {};
10
+ // Read during render by every collection view, so an unreadable value here
11
+ // used to throw the view away rather than fall back to an unconfigured one.
12
+ const getCollectionFromStorage = useCallback(<M extends Record<string, any>>(storageKey: string): PartialCollectionConfig<M> => {
13
+ return readStoredJson<PartialCollectionConfig<M>>(storageKey, { fallback: {}, accept: isRecordValue });
13
14
  }, []);
14
15
 
15
16
  const getCollectionConfig = useCallback(<M extends Record<string, any>>(path: string): PartialCollectionConfig<M> => {
@@ -22,9 +23,15 @@ export function useBuildLocalConfigurationPersistence(): UserConfigurationPersis
22
23
 
23
24
  const onCollectionModified = useCallback(<M extends Record<string, any>>(path: string, data: PartialCollectionConfig<M>) => {
24
25
  const storageKey = `collection_config::${stripCollectionPath(path)}`;
25
- localStorage.setItem(storageKey, JSON.stringify(data));
26
+ writeStoredJson(storageKey, data);
26
27
  const cachedConfig = configCache.current[storageKey];
27
- const newConfig = mergeDeep(cachedConfig ?? getCollectionFromStorage(path), data);
28
+ // `getCollectionFromStorage` takes the storage key, not the path — every
29
+ // other caller passes one. Reading `path` looked up a key this hook
30
+ // never writes, so the fallback could only ever contribute `{}`. It is
31
+ // masked today because the one caller merges the stored config in
32
+ // before calling, but `onCollectionModified` accepts a partial by type
33
+ // and by its public interface, and a real partial would lose the rest.
34
+ const newConfig = mergeDeep(cachedConfig ?? getCollectionFromStorage(storageKey), data);
28
35
  configCache.current[storageKey] = mergeDeep(configCache.current[storageKey], newConfig);
29
36
  }, [getCollectionFromStorage]);
30
37
 
@@ -33,23 +40,24 @@ export function useBuildLocalConfigurationPersistence(): UserConfigurationPersis
33
40
  const [collapsedGroups, _setCollapsedGroups] = useState<string[]>([]);
34
41
 
35
42
  useEffect(() => {
36
- _setRecentlyVisitedPaths(localStorage.getItem("recently_visited_paths") ? JSON.parse(localStorage.getItem("recently_visited_paths")!) : []);
37
- _setFavouritePaths(localStorage.getItem("favourite_paths") ? JSON.parse(localStorage.getItem("favourite_paths")!) : []);
38
- _setCollapsedGroups(localStorage.getItem("collapsed_groups") ? JSON.parse(localStorage.getItem("collapsed_groups")!) : []);
43
+ const readPaths = (key: string) => readStoredJson<string[]>(key, { fallback: [], accept: isArrayValue });
44
+ _setRecentlyVisitedPaths(readPaths("recently_visited_paths"));
45
+ _setFavouritePaths(readPaths("favourite_paths"));
46
+ _setCollapsedGroups(readPaths("collapsed_groups"));
39
47
  }, []);
40
48
 
41
49
  const setRecentlyVisitedPaths = useCallback((paths: string[]) => {
42
- localStorage.setItem("recently_visited_paths", JSON.stringify(paths));
50
+ writeStoredJson("recently_visited_paths", paths);
43
51
  _setRecentlyVisitedPaths(paths);
44
52
  }, []);
45
53
 
46
54
  const setFavouritePaths = useCallback((paths: string[]) => {
47
- localStorage.setItem("favourite_paths", JSON.stringify(paths));
55
+ writeStoredJson("favourite_paths", paths);
48
56
  _setFavouritePaths(paths);
49
57
  }, []);
50
58
 
51
59
  const setCollapsedGroups = useCallback((paths: string[]) => {
52
- localStorage.setItem("collapsed_groups", JSON.stringify(paths));
60
+ writeStoredJson("collapsed_groups", paths);
53
61
  _setCollapsedGroups(paths);
54
62
  }, []);
55
63
 
package/src/locales/de.ts CHANGED
@@ -77,6 +77,7 @@ export const de: RebaseTranslations = {
77
77
  column_cannot_be_edited: "Diese Spalte kann nicht direkt bearbeitet werden",
78
78
  close: "Schließen",
79
79
  hide_list: "Liste ausblenden",
80
+ show_list: "Liste einblenden",
80
81
  unsaved_local_changes: "Ungespeicherte lokale Änderungen",
81
82
  unsaved_local_changes_description: "Dieses Dokument wurde lokal bearbeitet und weist ungespeicherte Änderungen auf. Diese lokalen Änderungen gehen verloren, wenn Sie sie nicht anwenden.",
82
83
  preview_changes: "Änderungen in der Vorschau anzeigen",
@@ -412,6 +413,7 @@ export const de: RebaseTranslations = {
412
413
  flatten_arrays: "Arrays glätten",
413
414
  download: "Herunterladen",
414
415
  large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
416
+ too_many_documents_to_export: "Diese Sammlung enthält mehr Dokumente ({{count}}), als der Browser in einer einzigen Datei exportieren kann (Grenze {{limit}}). Filtern Sie sie vor dem Export.",
415
417
  include_undefined_values: "Undefinierte Werte einschließen",
416
418
  submit: "Einreichen",
417
419
 
package/src/locales/en.ts CHANGED
@@ -87,6 +87,7 @@ export const en: RebaseTranslations = {
87
87
  column_cannot_be_edited: "This column can't be edited directly",
88
88
  close: "Close",
89
89
  hide_list: "Hide list",
90
+ show_list: "Show list",
90
91
  unsaved_local_changes: "Unsaved Local changes",
91
92
  unsaved_local_changes_description: "This document was edited locally and has unsaved changes. These local changes will be lost if you don't apply them.",
92
93
  preview_changes: "Preview Changes",
@@ -425,6 +426,7 @@ export const en: RebaseTranslations = {
425
426
  flatten_arrays: "Flatten arrays",
426
427
  download: "Download",
427
428
  large_number_of_documents: "This collection has a large number of documents ({{count}}).",
429
+ too_many_documents_to_export: "This collection has more documents ({{count}}) than the browser can export in a single file (limit {{limit}}). Filter it down before exporting.",
428
430
  include_undefined_values: "Include undefined values",
429
431
  submit: "Submit",
430
432
 
@@ -993,6 +995,7 @@ export const en: RebaseTranslations = {
993
995
  inspect_record: "Inspect record",
994
996
  inspect: "Inspect",
995
997
  close_inspector: "Close inspector",
998
+ open_in_live_site: "Open in the live site",
996
999
 
997
1000
  // ─── Revision history ───────────────────────────────────────
998
1001
  history_revert: "Revert to this version",
package/src/locales/es.ts CHANGED
@@ -85,6 +85,7 @@ export const es: RebaseTranslations = {
85
85
  column_cannot_be_edited: "Esta columna no se puede editar directamente",
86
86
  close: "Cerrar",
87
87
  hide_list: "Ocultar lista",
88
+ show_list: "Mostrar lista",
88
89
  unsaved_local_changes: "Cambios locales sin guardar",
89
90
  unsaved_local_changes_description: "Este documento ha sido editado localmente y tiene cambios sin guardar. Estos cambios locales se perderán si no los aplicas.",
90
91
  preview_changes: "Previsualizar cambios",
@@ -420,6 +421,7 @@ export const es: RebaseTranslations = {
420
421
  flatten_arrays: "Aplanar arrays",
421
422
  download: "Descargar",
422
423
  large_number_of_documents: "Esta colección posee un gran número de documentos ({{count}}).",
424
+ too_many_documents_to_export: "Esta colección tiene más documentos ({{count}}) de los que el navegador puede exportar en un solo archivo (límite {{limit}}). Fíltrala antes de exportar.",
423
425
  include_undefined_values: "Incluir valores omitidos (undefined)",
424
426
  submit: "Enviar",
425
427
 
package/src/locales/fr.ts CHANGED
@@ -77,6 +77,7 @@ export const fr: RebaseTranslations = {
77
77
  column_cannot_be_edited: "Cette colonne ne peut pas être modifiée directement",
78
78
  close: "Fermer",
79
79
  hide_list: "Masquer la liste",
80
+ show_list: "Afficher la liste",
80
81
  unsaved_local_changes: "Modifications locales non enregistrées",
81
82
  unsaved_local_changes_description: "Ce document a été modifié localement et contient des modifications non enregistrées. Ces modifications locales seront perdues si vous ne les appliquez pas.",
82
83
  preview_changes: "Aperçu des modifications",
@@ -412,6 +413,7 @@ export const fr: RebaseTranslations = {
412
413
  flatten_arrays: "Aplatir les tableaux",
413
414
  download: "Télécharger",
414
415
  large_number_of_documents: "Cette collection contient un grand nombre de documents ({{count}}).",
416
+ too_many_documents_to_export: "Cette collection contient plus de documents ({{count}}) que le navigateur ne peut en exporter dans un seul fichier (limite {{limit}}). Filtrez-la avant d'exporter.",
415
417
  include_undefined_values: "Inclure les valeurs non définies",
416
418
  submit: "Soumettre",
417
419
 
package/src/locales/hi.ts CHANGED
@@ -69,7 +69,7 @@ export const hi: RebaseTranslations = {
69
69
  group_by: "इसके द्वारा समूहित करें",
70
70
  initialize_kanban_order: "कानबन क्रम प्रारंभ करें",
71
71
  copy_id: "ID कॉपी करें",
72
- add_specific: "विशिष्ट {{name}} जोड़ें",
72
+ add_specific: "{{name}} जोड़ें",
73
73
  select_specific: "विशिष्ट {{name}} चुनें",
74
74
  select_from: "{{name}} से चुनें",
75
75
  done: "हो गया",
@@ -77,6 +77,7 @@ export const hi: RebaseTranslations = {
77
77
  column_cannot_be_edited: "इस कॉलम को सीधे संपादित नहीं किया जा सकता",
78
78
  close: "बंद करें",
79
79
  hide_list: "सूची छिपाएँ",
80
+ show_list: "सूची दिखाएँ",
80
81
  unsaved_local_changes: "सहेजे नहीं गए स्थानीय परिवर्तन",
81
82
  unsaved_local_changes_description: "इस दस्तावेज़ को स्थानीय स्तर पर संपादित किया गया था और इसमें सहेजे नहीं गए परिवर्तन हैं। यदि आप इन्हें लागू नहीं करते हैं तो ये स्थानीय परिवर्तन खो जाएंगे।",
82
83
  preview_changes: "परिवर्तनों का पूर्वावलोकन करें",
@@ -412,6 +413,7 @@ export const hi: RebaseTranslations = {
412
413
  flatten_arrays: "ऐरे को फ़्लैट करें",
413
414
  download: "डाउनलोड",
414
415
  large_number_of_documents: "इस संग्रह में बड़ी संख्या में दस्तावेज़ हैं ({{count}})।",
416
+ too_many_documents_to_export: "इस संग्रह में इतने दस्तावेज़ ({{count}}) हैं कि ब्राउज़र उन्हें एक ही फ़ाइल में निर्यात नहीं कर सकता (सीमा {{limit}})। निर्यात से पहले इसे फ़िल्टर करें।",
415
417
  include_undefined_values: "अपरिभाषित (undefined) मान शामिल करें",
416
418
  submit: "सबमिट करें",
417
419
 
package/src/locales/it.ts CHANGED
@@ -77,6 +77,7 @@ export const it: RebaseTranslations = {
77
77
  column_cannot_be_edited: "Questa colonna non può essere modificata direttamente",
78
78
  close: "Chiudi",
79
79
  hide_list: "Nascondi elenco",
80
+ show_list: "Mostra elenco",
80
81
  unsaved_local_changes: "Modifiche locali non salvate",
81
82
  unsaved_local_changes_description: "Questo documento è stato modificato localmente e ha modifiche non salvate. Queste modifiche locali andranno perse se non le applichi.",
82
83
  preview_changes: "Anteprima modifiche",
@@ -412,6 +413,7 @@ export const it: RebaseTranslations = {
412
413
  flatten_arrays: "Appiattisci gli array",
413
414
  download: "Scarica",
414
415
  large_number_of_documents: "Questa collezione contiene un numero elevato di documenti ({{count}})",
416
+ too_many_documents_to_export: "Questa collezione contiene più documenti ({{count}}) di quanti il browser possa esportarne in un solo file (limite {{limit}}). Filtrala prima di esportare.",
415
417
  include_undefined_values: "Includi valori non definiti",
416
418
  submit: "Invia",
417
419
 
package/src/locales/pt.ts CHANGED
@@ -82,6 +82,7 @@ export const pt: RebaseTranslations = {
82
82
  column_cannot_be_edited: "Esta coluna não pode ser editada diretamente",
83
83
  close: "Fechar",
84
84
  hide_list: "Ocultar lista",
85
+ show_list: "Mostrar lista",
85
86
  unsaved_local_changes: "Alterações locais não guardadas",
86
87
  unsaved_local_changes_description: "Este documento foi editado localmente e tem alterações não guardadas. Estas alterações locais serão perdidas se não as aplicar.",
87
88
  preview_changes: "Pré-visualizar alterações",
@@ -417,6 +418,7 @@ export const pt: RebaseTranslations = {
417
418
  flatten_arrays: "Achatar listas",
418
419
  download: "Descarregar",
419
420
  large_number_of_documents: "Esta coleção tem um grande número de documentos ({{count}}).",
421
+ too_many_documents_to_export: "Esta coleção tem mais documentos ({{count}}) do que o navegador consegue exportar num único ficheiro (limite {{limit}}). Filtre-a antes de exportar.",
420
422
  include_undefined_values: "Incluir valores indefinidos",
421
423
  submit: "Submeter",
422
424