@rebasepro/admin 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{CollectionEditorDialog-Dxx0ZaDk.js → CollectionEditorDialog-D2Vf4C3-.js} +7 -7
- package/dist/{CollectionEditorDialog-Dxx0ZaDk.js.map → CollectionEditorDialog-D2Vf4C3-.js.map} +1 -1
- package/dist/{PropertyEditView-D6xNTm0d.js → PropertyEditView-D3NBsK9_.js} +4 -13
- package/dist/PropertyEditView-D3NBsK9_.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-C4vSn9Pu.js → RouterCollectionsStudioView-hPB54l3Q.js} +4 -4
- package/dist/{RouterCollectionsStudioView-C4vSn9Pu.js.map → RouterCollectionsStudioView-hPB54l3Q.js.map} +1 -1
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +9 -1
- package/dist/components/RelationSelector.d.ts +8 -0
- package/dist/{export-BZi1MzQ5.js → export-oauWlxuu.js} +2 -2
- package/dist/{export-BZi1MzQ5.js.map → export-oauWlxuu.js.map} +1 -1
- package/dist/form/useUndoableDiscard.d.ts +15 -0
- package/dist/{history-B5pkfldE.js → history-C2cqc0bw.js} +2 -2
- package/dist/{history-B5pkfldE.js.map → history-C2cqc0bw.js.map} +1 -1
- package/dist/{import-CXpuYo-T.js → import-Bxv5JPm-.js} +2 -2
- package/dist/{import-CXpuYo-T.js.map → import-Bxv5JPm-.js.map} +1 -1
- package/dist/index.js +17 -25
- package/dist/index.js.map +1 -1
- package/dist/{util-BKtHPLe4.js → util-C4qsI1QV.js} +163 -55
- package/dist/util-C4qsI1QV.js.map +1 -0
- package/package.json +12 -9
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +4 -4
- package/src/components/CollectionPanel.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +3 -11
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +32 -2
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +10 -8
- package/src/components/DefaultAppBar.tsx +0 -10
- package/src/components/DetailViewBinding.tsx +18 -1
- package/src/components/EditViewBinding.tsx +26 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +10 -1
- package/src/components/RelationSelector.tsx +115 -3
- package/src/contexts/BreacrumbsContext.tsx +4 -21
- package/src/form/EntityForm.tsx +8 -3
- package/src/form/components/FormRail.tsx +15 -4
- package/src/form/useUndoableDiscard.ts +39 -0
- package/src/preview/components/StorageThumbnail.tsx +35 -1
- package/src/routes/RebaseRoute.tsx +2 -10
- package/dist/PropertyEditView-D6xNTm0d.js.map +0 -1
- package/dist/util-BKtHPLe4.js.map +0 -1
|
@@ -36,10 +36,21 @@ export function FormRail({
|
|
|
36
36
|
// `w-76` (304px), between the old 288 and a full `w-80`. The rail
|
|
37
37
|
// holds real controls — a status select, a date picker — plus a
|
|
38
38
|
// record block whose id is a 36-character UUID, so 288 left the
|
|
39
|
-
// select tighter than the identical select in the column.
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
39
|
+
// select tighter than the identical select in the column.
|
|
40
|
+
//
|
|
41
|
+
// 304 is the floor rather than the answer everywhere. It was chosen
|
|
42
|
+
// against the *narrow* end, where 320 took more than it gave back —
|
|
43
|
+
// past about 300px the extra went to the gap beside a chip. That
|
|
44
|
+
// stops being true once the form is genuinely wide: `@7xl/form`
|
|
45
|
+
// (1280px of form) is the same signal the column widens on, and by
|
|
46
|
+
// then 32px is coming out of gutters, not out of the column.
|
|
47
|
+
//
|
|
48
|
+
// Container, not viewport, for the reason the whole rail is
|
|
49
|
+
// measured rather than breakpointed: a side panel inside a large
|
|
50
|
+
// window is not a wide form. Both surfaces that render a rail —
|
|
51
|
+
// `EntityForm` and `EntityViewBinding` — name their form element
|
|
52
|
+
// `@container/form`, so a panel that never gets that wide keeps 304.
|
|
53
|
+
"flex flex-col gap-6 shrink-0 w-76 @7xl/form:w-84 border-l overflow-y-auto",
|
|
43
54
|
"px-5 py-6 bg-surface-50 dark:bg-surface-900",
|
|
44
55
|
defaultBorderMixin
|
|
45
56
|
)}>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { EntityStatus } from "@rebasepro/types";
|
|
3
|
+
import type { FormexController } from "@rebasepro/forms";
|
|
4
|
+
import { useSafeSnackbarController } from "../collection_editor/useSafeSnackbarController";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Throw away everything the user has typed, reversibly.
|
|
8
|
+
*
|
|
9
|
+
* "Discard" and "Clear" sit next to Save, take one click, and undo everything
|
|
10
|
+
* the user has done since they opened the record — the identity bar's version
|
|
11
|
+
* does not even stop to ask. So the reset goes *through* the undo history
|
|
12
|
+
* rather than around it (see `undoable` in `FormexResetProps`), and the
|
|
13
|
+
* confirmation carries the Undo, which is the only place it can be: the form
|
|
14
|
+
* has no undo button, only the ⌘Z the user has no reason to guess at.
|
|
15
|
+
*
|
|
16
|
+
* Pass `values` to reset to something other than the form's current baseline.
|
|
17
|
+
*/
|
|
18
|
+
export function useUndoableDiscard() {
|
|
19
|
+
|
|
20
|
+
const snackbarController = useSafeSnackbarController();
|
|
21
|
+
|
|
22
|
+
return useCallback((
|
|
23
|
+
formex: FormexController<any>,
|
|
24
|
+
status: EntityStatus,
|
|
25
|
+
values?: unknown
|
|
26
|
+
) => {
|
|
27
|
+
formex.resetForm(values !== undefined
|
|
28
|
+
? { values, undoable: true }
|
|
29
|
+
: { undoable: true });
|
|
30
|
+
snackbarController?.open({
|
|
31
|
+
type: "info",
|
|
32
|
+
message: status === "existing" ? "Changes discarded" : "Form cleared",
|
|
33
|
+
action: {
|
|
34
|
+
label: "Undo",
|
|
35
|
+
onClick: () => formex.undo()
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}, [snackbarController]);
|
|
39
|
+
}
|
|
@@ -33,6 +33,40 @@ function areEqual(prevProps: StorageThumbnailProps, nextProps: StorageThumbnailP
|
|
|
33
33
|
|
|
34
34
|
const URL_CACHE: Record<string, DownloadConfig> = {};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The signed-URL request currently in flight for a given cache key.
|
|
38
|
+
*
|
|
39
|
+
* A collection view draws one thumbnail per row and rows share images far more
|
|
40
|
+
* often than not — the demo's 200 blog posts are illustrated by 20 hero images.
|
|
41
|
+
* Every one of those thumbnails mounts in the same tick, and each used to call
|
|
42
|
+
* `getSignedUrl` on its own: {@link URL_CACHE} is only written when a response
|
|
43
|
+
* *lands*, so it is still empty while the burst goes out and dedupes nothing.
|
|
44
|
+
* The result was five or more identical requests per distinct file, and minting
|
|
45
|
+
* a download token is a real request — `/api/storage/metadata/<path>` — against
|
|
46
|
+
* a rate-limited surface. One page view was enough to exhaust the budget and
|
|
47
|
+
* every thumbnail on the screen then failed with a 429.
|
|
48
|
+
*
|
|
49
|
+
* Sharing the promise collapses that burst to one request per file. It
|
|
50
|
+
* deliberately does not extend how long anything is cached: `DownloadConfig.url`
|
|
51
|
+
* is temporal, so a later mount still refetches exactly as it did before.
|
|
52
|
+
*/
|
|
53
|
+
const IN_FLIGHT = new Map<string, Promise<DownloadConfig>>();
|
|
54
|
+
|
|
55
|
+
function getSignedUrlOnce(
|
|
56
|
+
storage: { getSignedUrl: (path: string) => Promise<DownloadConfig> },
|
|
57
|
+
path: string,
|
|
58
|
+
cacheKey: string
|
|
59
|
+
): Promise<DownloadConfig> {
|
|
60
|
+
const existing = IN_FLIGHT.get(cacheKey);
|
|
61
|
+
if (existing) return existing;
|
|
62
|
+
|
|
63
|
+
const request = storage.getSignedUrl(path).finally(() => {
|
|
64
|
+
IN_FLIGHT.delete(cacheKey);
|
|
65
|
+
});
|
|
66
|
+
IN_FLIGHT.set(cacheKey, request);
|
|
67
|
+
return request;
|
|
68
|
+
}
|
|
69
|
+
|
|
36
70
|
export function StorageThumbnailInternal({
|
|
37
71
|
storeUrl,
|
|
38
72
|
interactive,
|
|
@@ -64,7 +98,7 @@ export function StorageThumbnailInternal({
|
|
|
64
98
|
if (!storagePathOrDownloadUrl)
|
|
65
99
|
return;
|
|
66
100
|
let unmounted = false;
|
|
67
|
-
storage
|
|
101
|
+
getSignedUrlOnce(storage, storagePathOrDownloadUrl, cacheKey)
|
|
68
102
|
.then(function (downloadConfig) {
|
|
69
103
|
if (!unmounted) {
|
|
70
104
|
setDownloadConfig(downloadConfig);
|
|
@@ -41,32 +41,24 @@ export function RebaseRoute() {
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
useEffect(() => {
|
|
44
|
-
const lastEntry = navigationEntries[navigationEntries.length - 1];
|
|
45
|
-
const isViewingCollection = lastEntry?.type === "collection";
|
|
46
|
-
|
|
47
44
|
breadcrumbs.set({
|
|
48
|
-
breadcrumbs: navigationEntries.map((entry
|
|
49
|
-
const isLastEntry = index === navigationEntries.length - 1;
|
|
45
|
+
breadcrumbs: navigationEntries.map((entry) => {
|
|
50
46
|
|
|
51
47
|
if (entry.type === "entity") {
|
|
52
48
|
return ({
|
|
53
49
|
title: String(entry.entityId),
|
|
54
50
|
url: urlController.buildUrlCollectionPath(entry.path)
|
|
55
|
-
// count: undefined (not applicable for entities)
|
|
56
51
|
});
|
|
57
52
|
} else if (entry.type === "custom_view") {
|
|
58
53
|
return ({
|
|
59
54
|
title: String(entry.view.name ?? entry.view.key),
|
|
60
55
|
url: urlController.buildUrlCollectionPath(entry.path)
|
|
61
|
-
// count: undefined (not applicable for custom views)
|
|
62
56
|
});
|
|
63
57
|
} else if (entry.type === "collection") {
|
|
64
|
-
const showCount = isLastEntry && isViewingCollection;
|
|
65
58
|
return ({
|
|
66
59
|
title: entry.collection.name,
|
|
67
60
|
url: urlController.buildUrlCollectionPath(entry.path),
|
|
68
|
-
id: entry.path
|
|
69
|
-
...(showCount ? { count: null } : {})
|
|
61
|
+
id: entry.path
|
|
70
62
|
});
|
|
71
63
|
} else {
|
|
72
64
|
throw new Error("Unexpected navigation entry type");
|