@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.
Files changed (40) hide show
  1. package/dist/{CollectionEditorDialog-Dxx0ZaDk.js → CollectionEditorDialog-D2Vf4C3-.js} +7 -7
  2. package/dist/{CollectionEditorDialog-Dxx0ZaDk.js.map → CollectionEditorDialog-D2Vf4C3-.js.map} +1 -1
  3. package/dist/{PropertyEditView-D6xNTm0d.js → PropertyEditView-D3NBsK9_.js} +4 -13
  4. package/dist/PropertyEditView-D3NBsK9_.js.map +1 -0
  5. package/dist/{RouterCollectionsStudioView-C4vSn9Pu.js → RouterCollectionsStudioView-hPB54l3Q.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-C4vSn9Pu.js.map → RouterCollectionsStudioView-hPB54l3Q.js.map} +1 -1
  7. package/dist/collection_editor_ui.js +4 -4
  8. package/dist/components/CollectionViewBinding/CollectionViewStartActions.d.ts +9 -1
  9. package/dist/components/RelationSelector.d.ts +8 -0
  10. package/dist/{export-BZi1MzQ5.js → export-oauWlxuu.js} +2 -2
  11. package/dist/{export-BZi1MzQ5.js.map → export-oauWlxuu.js.map} +1 -1
  12. package/dist/form/useUndoableDiscard.d.ts +15 -0
  13. package/dist/{history-B5pkfldE.js → history-C2cqc0bw.js} +2 -2
  14. package/dist/{history-B5pkfldE.js.map → history-C2cqc0bw.js.map} +1 -1
  15. package/dist/{import-CXpuYo-T.js → import-Bxv5JPm-.js} +2 -2
  16. package/dist/{import-CXpuYo-T.js.map → import-Bxv5JPm-.js.map} +1 -1
  17. package/dist/index.js +17 -25
  18. package/dist/index.js.map +1 -1
  19. package/dist/{util-BKtHPLe4.js → util-C4qsI1QV.js} +163 -55
  20. package/dist/util-C4qsI1QV.js.map +1 -0
  21. package/package.json +12 -9
  22. package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +4 -4
  23. package/src/components/CollectionPanel.tsx +1 -1
  24. package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +1 -1
  25. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +3 -11
  26. package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +32 -2
  27. package/src/components/CollectionViewBinding/EntityCardBinding.tsx +10 -8
  28. package/src/components/DefaultAppBar.tsx +0 -10
  29. package/src/components/DetailViewBinding.tsx +18 -1
  30. package/src/components/EditViewBinding.tsx +26 -2
  31. package/src/components/ReferenceTable/SelectionTableBinding.tsx +10 -1
  32. package/src/components/RelationSelector.tsx +115 -3
  33. package/src/contexts/BreacrumbsContext.tsx +4 -21
  34. package/src/form/EntityForm.tsx +8 -3
  35. package/src/form/components/FormRail.tsx +15 -4
  36. package/src/form/useUndoableDiscard.ts +39 -0
  37. package/src/preview/components/StorageThumbnail.tsx +35 -1
  38. package/src/routes/RebaseRoute.tsx +2 -10
  39. package/dist/PropertyEditView-D6xNTm0d.js.map +0 -1
  40. 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. 320 took
40
- // more than it gave back: past about 300px the extra goes to the
41
- // gap beside a chip, not to anything that needed it.
42
- "flex flex-col gap-6 shrink-0 w-76 border-l overflow-y-auto",
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.getSignedUrl(storagePathOrDownloadUrl)
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, index) => {
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");