@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.
- package/dist/collections/entity-display-cache.d.ts +75 -0
- package/dist/collections/entity-display.d.ts +35 -0
- package/dist/collections/entity_image_preview.d.ts +8 -0
- package/dist/collections/form-layout.d.ts +6 -0
- package/dist/collections/index.d.ts +4 -0
- package/dist/collections/property-path.d.ts +16 -0
- package/dist/collections/property_presentation.d.ts +18 -3
- package/dist/collections/summary-property.d.ts +65 -0
- package/dist/components/LoginView/oauth-redirect-flow.d.ts +86 -0
- package/dist/components/common/useColumnsIds.d.ts +22 -0
- package/dist/core/Rebase.d.ts +1 -1
- package/dist/core/RebaseProps.d.ts +10 -2
- package/dist/hooks/data/collectionQuery.d.ts +45 -0
- package/dist/hooks/data/useRelationSelector.d.ts +12 -1
- package/dist/index.es.js +866 -241
- package/dist/index.es.js.map +1 -1
- package/dist/util/entity_cache.d.ts +19 -3
- package/dist/util/previews.d.ts +19 -0
- package/package.json +7 -7
- package/src/auth/useRebaseAuthController.ts +15 -1
- package/src/collections/entity-display-cache.ts +195 -0
- package/src/collections/entity-display.ts +105 -0
- package/src/collections/entity_image_preview.ts +13 -0
- package/src/collections/form-layout.ts +8 -1
- package/src/collections/index.ts +4 -0
- package/src/collections/property-path.ts +30 -0
- package/src/collections/property_presentation.ts +39 -4
- package/src/collections/summary-property.ts +114 -0
- package/src/collections/title-property.ts +13 -16
- package/src/components/LoginView/LoginView.tsx +48 -26
- package/src/components/LoginView/oauth-redirect-flow.ts +184 -0
- package/src/components/common/useColumnsIds.tsx +67 -29
- package/src/components/common/useDataTableController.tsx +10 -14
- package/src/core/Rebase.tsx +20 -2
- package/src/core/RebaseProps.tsx +10 -2
- package/src/hooks/data/collectionQuery.ts +55 -0
- package/src/hooks/data/useCollection.tsx +29 -21
- package/src/hooks/data/useFetch.tsx +17 -2
- package/src/hooks/data/useRelationSelector.tsx +46 -8
- package/src/hooks/useAuthSubscription.ts +18 -2
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +20 -12
- package/src/locales/de.ts +2 -0
- package/src/locales/en.ts +3 -0
- package/src/locales/es.ts +2 -0
- package/src/locales/fr.ts +2 -0
- package/src/locales/hi.ts +3 -1
- package/src/locales/it.ts +2 -0
- package/src/locales/pt.ts +2 -0
- package/src/util/entity_cache.ts +34 -31
- package/src/util/previews.ts +49 -29
package/src/core/Rebase.tsx
CHANGED
|
@@ -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);
|
package/src/core/RebaseProps.tsx
CHANGED
|
@@ -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
|
-
|
|
140
|
-
|
|
141
|
-
limit: itemCount,
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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) =>
|
|
122
|
-
|
|
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) =>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
26
|
+
writeStoredJson(storageKey, data);
|
|
26
27
|
const cachedConfig = configCache.current[storageKey];
|
|
27
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
50
|
+
writeStoredJson("recently_visited_paths", paths);
|
|
43
51
|
_setRecentlyVisitedPaths(paths);
|
|
44
52
|
}, []);
|
|
45
53
|
|
|
46
54
|
const setFavouritePaths = useCallback((paths: string[]) => {
|
|
47
|
-
|
|
55
|
+
writeStoredJson("favourite_paths", paths);
|
|
48
56
|
_setFavouritePaths(paths);
|
|
49
57
|
}, []);
|
|
50
58
|
|
|
51
59
|
const setCollapsedGroups = useCallback((paths: string[]) => {
|
|
52
|
-
|
|
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: "
|
|
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
|
|