@rebasepro/admin 0.14.1 → 0.14.2-canary.g27a129e

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/dist/{CollectionEditorDialog-BeS4lxGh.js → CollectionEditorDialog-Dxx0ZaDk.js} +11 -5
  2. package/dist/CollectionEditorDialog-Dxx0ZaDk.js.map +1 -0
  3. package/dist/{PropertyEditView-BFJ6H9-Q.js → PropertyEditView-D6xNTm0d.js} +2 -2
  4. package/dist/{PropertyEditView-BFJ6H9-Q.js.map → PropertyEditView-D6xNTm0d.js.map} +1 -1
  5. package/dist/{RouterCollectionsStudioView-sfsUpEJz.js → RouterCollectionsStudioView-C4vSn9Pu.js} +4 -4
  6. package/dist/{RouterCollectionsStudioView-sfsUpEJz.js.map → RouterCollectionsStudioView-C4vSn9Pu.js.map} +1 -1
  7. package/dist/collection_editor/serializable_types.d.ts +6 -0
  8. package/dist/collection_editor_ui.js +4 -4
  9. package/dist/components/CollectionViewBinding/ViewModeToggle.d.ts +11 -5
  10. package/dist/components/RebaseAdmin.d.ts +1 -1
  11. package/dist/{export-KEf57Jso.js → export-BZi1MzQ5.js} +2 -2
  12. package/dist/{export-KEf57Jso.js.map → export-BZi1MzQ5.js.map} +1 -1
  13. package/dist/{history-Ctd2cRn_.js → history-B5pkfldE.js} +2 -2
  14. package/dist/{history-Ctd2cRn_.js.map → history-B5pkfldE.js.map} +1 -1
  15. package/dist/{import-BujhycMq.js → import-CXpuYo-T.js} +2 -2
  16. package/dist/{import-BujhycMq.js.map → import-CXpuYo-T.js.map} +1 -1
  17. package/dist/index.js +19 -14
  18. package/dist/index.js.map +1 -1
  19. package/dist/util/resolutions.d.ts +10 -1
  20. package/dist/util/view_mode.d.ts +24 -6
  21. package/dist/{util-bL-fk6C6.js → util-BKtHPLe4.js} +141 -52
  22. package/dist/util-BKtHPLe4.js.map +1 -0
  23. package/package.json +9 -9
  24. package/src/collection_editor/serializable_types.ts +6 -0
  25. package/src/collection_editor/serializable_utils.ts +8 -0
  26. package/src/collection_editor/ui/collection_editor/ViewModeSwitch.tsx +16 -2
  27. package/src/components/CollectionEditorDialogs.tsx +4 -3
  28. package/src/components/CollectionViewBinding/CollectionViewActions.tsx +4 -4
  29. package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +99 -22
  30. package/src/components/CollectionViewBinding/ViewModeToggle.tsx +66 -32
  31. package/src/components/EntityInspector.tsx +3 -2
  32. package/src/components/HomePage/NavigationCardBinding.tsx +2 -2
  33. package/src/components/RebaseAdmin.tsx +3 -2
  34. package/src/components/RebaseNavigation.tsx +13 -6
  35. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +3 -2
  36. package/src/util/resolutions.ts +30 -1
  37. package/src/util/view_mode.ts +49 -12
  38. package/dist/CollectionEditorDialog-BeS4lxGh.js.map +0 -1
  39. package/dist/util-bL-fk6C6.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useRef, useEffect, useCallback, useContext, lazy, Suspense } from "react";
1
+ import React, { useMemo, useRef, useEffect, useCallback, useContext, Suspense } from "react";
2
2
  import {
3
3
  useRebaseRegistry,
4
4
  useRebaseContext,
@@ -16,9 +16,9 @@ import {
16
16
  RebaseDataContext
17
17
  } from "@rebasepro/app";
18
18
  import { buildRoutedRebaseData, resolveDataSource } from "@rebasepro/common";
19
- import { CircularProgressCenter } from "@rebasepro/ui";
19
+ import { CircularProgressCenter, lazyChunk } from "@rebasepro/ui";
20
20
 
21
- import type { AppView, CollectionEditorOptions, EntityCustomView, EntityAction, RebasePlugin, AdminCollection } from "@rebasepro/admin-types";
21
+ import type { AppView, CollectionCustomView, CollectionEditorOptions, EntityCustomView, EntityAction, RebasePlugin, AdminCollection } from "@rebasepro/admin-types";
22
22
  import type { CollectionRegistryController } from "@rebasepro/types";
23
23
  import type { UrlController, NavigationStateController } from "@rebasepro/admin-types";
24
24
 
@@ -41,7 +41,7 @@ import { ConfigControllerProvider } from "../collection_editor/ConfigControllerP
41
41
  import { useCollectionsConfigController } from "../collection_editor/useCollectionsConfigController";
42
42
 
43
43
  // Lazy-load the schema view — only fetched when studio schema tool is active
44
- const CollectionsStudioView = lazy(() =>
44
+ const CollectionsStudioView = lazyChunk(() =>
45
45
  import("../collection_editor/ui/collection_editor/RouterCollectionsStudioView")
46
46
  .then(m => ({ default: m.RouterCollectionsStudioView }))
47
47
  );
@@ -260,7 +260,10 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
260
260
  const enrichedCustomizationController = useMemo(() => {
261
261
  const cmsEntityViews = (registry.cmsConfig?.entityViews ?? []) as EntityCustomView[];
262
262
  const cmsEntityActions = (registry.cmsConfig?.entityActions ?? []) as EntityAction[];
263
- if (cmsEntityViews.length === 0 && cmsEntityActions.length === 0) {
263
+ // Collection view modes ride the same channel, for the same reason:
264
+ // `admin.customViews: ["map"]` has to find the component somewhere.
265
+ const cmsCollectionViews = (registry.cmsConfig?.collectionViews ?? []) as CollectionCustomView[];
266
+ if (cmsEntityViews.length === 0 && cmsEntityActions.length === 0 && cmsCollectionViews.length === 0) {
264
267
  return parentCustomizationController;
265
268
  }
266
269
  return {
@@ -272,9 +275,13 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
272
275
  entityActions: [
273
276
  ...(parentCustomizationController.entityActions ?? []),
274
277
  ...cmsEntityActions.filter(a => !(parentCustomizationController.entityActions ?? []).some(ea => ea.key === a.key))
278
+ ],
279
+ collectionViews: [
280
+ ...(parentCustomizationController.collectionViews ?? []),
281
+ ...cmsCollectionViews.filter(v => !(parentCustomizationController.collectionViews ?? []).some(cv => cv.key === v.key))
275
282
  ]
276
283
  };
277
- }, [parentCustomizationController, registry.cmsConfig?.entityViews, registry.cmsConfig?.entityActions]);
284
+ }, [parentCustomizationController, registry.cmsConfig?.entityViews, registry.cmsConfig?.entityActions, registry.cmsConfig?.collectionViews]);
278
285
 
279
286
  // ── Inner content with all context providers ──────────────────────
280
287
  // Re-provide RebaseDataContext with the routed data so that every admin
@@ -1,4 +1,4 @@
1
- import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { FieldHelperText } from "../components/FieldHelperText";
3
3
  import { LabelWithIconAndTooltip } from "../components/LabelWithIconAndTooltip";
4
4
  import { useAuthController, useStorageSource } from "@rebasepro/app";
@@ -13,6 +13,7 @@ import {
13
13
  fieldBackgroundHoverMixin,
14
14
  fieldBackgroundMixin,
15
15
  IconButton,
16
+ lazyChunk,
16
17
  Skeleton,
17
18
  XIcon
18
19
  } from "@rebasepro/ui";
@@ -22,7 +23,7 @@ import { randomString } from "@rebasepro/utils";
22
23
 
23
24
  // Lazy-load ProseMirror editor + markdown parser/serializer (~300KB)
24
25
  // Only fetched when a markdown field is actually rendered.
25
- const RichTextEditor = lazy(() => import("../../editor").then(m => ({ default: m.RichTextEditor })));
26
+ const RichTextEditor = lazyChunk(() => import("../../editor").then(m => ({ default: m.RichTextEditor })));
26
27
  const loadMarkdownUtils = () => import("../../editor/markdown");
27
28
  let _markdownUtils: Awaited<ReturnType<typeof loadMarkdownUtils>> | null = null;
28
29
  const getMarkdownUtils = async () => {
@@ -1,6 +1,7 @@
1
1
  import { HistoryIcon } from "@rebasepro/ui";
2
2
  import React from "react";
3
- import type { CustomizationController, EntityAction, EntityCustomView } from "@rebasepro/admin-types";
3
+ import type { CollectionCustomView, CustomizationController, EntityAction, EntityCustomView } from "@rebasepro/admin-types";
4
+ import { VIEW_MODES } from "./view_mode";
4
5
 
5
6
  /**
6
7
  * Built-in entity views that are resolved by token name.
@@ -29,6 +30,34 @@ export function resolveEntityView(
29
30
  }
30
31
  }
31
32
 
33
+ /**
34
+ * Resolve a collection's `admin.customViews` into the views it can actually
35
+ * render, dropping every entry that names a key nothing registered.
36
+ *
37
+ * Dropping is deliberate: an unresolvable key must not become a switcher entry
38
+ * that renders a blank panel. It falls out of `enabledViews` too, so a user
39
+ * whose saved view mode was removed lands on a built-in.
40
+ */
41
+ export function resolveCollectionViews(
42
+ customViews: (string | CollectionCustomView<any>)[] | undefined,
43
+ contextCollectionViews?: CollectionCustomView<any>[]
44
+ ): CollectionCustomView<any>[] {
45
+ if (!customViews) return [];
46
+ const resolved: CollectionCustomView<any>[] = [];
47
+ const seen = new Set<string>();
48
+ for (const entry of customViews) {
49
+ const view = typeof entry === "string"
50
+ ? contextCollectionViews?.find((candidate) => candidate.key === entry)
51
+ : entry;
52
+ // A custom view keyed "table" would shadow a built-in in the switcher
53
+ // and be unreachable in the render chain, which checks built-ins first.
54
+ if (!view || VIEW_MODES.includes(view.key) || seen.has(view.key)) continue;
55
+ seen.add(view.key);
56
+ resolved.push(view);
57
+ }
58
+ return resolved;
59
+ }
60
+
32
61
  export function resolveEntityAction<M extends Record<string, unknown>>(
33
62
  entityAction: string | EntityAction<M>,
34
63
  contextEntityActions?: EntityAction<M>[]
@@ -1,11 +1,12 @@
1
1
 
2
- import type { ViewMode, AdminCollection } from "@rebasepro/admin-types";
2
+ import type { ViewMode, AdminCollection, CollectionCustomView } from "@rebasepro/admin-types";
3
3
 
4
4
  /**
5
5
  * Query param that carries the active collection view mode across navigations.
6
6
  */
7
7
  export const VIEW_MODE_PARAM = "__view";
8
8
 
9
+ /** The view modes every collection has, before custom views are added. */
9
10
  export const VIEW_MODES: ViewMode[] = ["list", "table", "cards", "kanban"];
10
11
 
11
12
  export const DEFAULT_VIEW_MODE: ViewMode = "list";
@@ -14,20 +15,34 @@ export type OpenEntityMode = "side_panel" | "full_screen" | "split" | "dialog";
14
15
 
15
16
  export const DEFAULT_OPEN_ENTITY_MODE: OpenEntityMode = "split";
16
17
 
17
- export function isViewMode(value: string | null | undefined): value is ViewMode {
18
- return Boolean(value) && VIEW_MODES.includes(value as ViewMode);
18
+ /**
19
+ * Whether `value` names a view mode this collection can actually render.
20
+ *
21
+ * `customKeys` are the keys of the collection's resolved custom views. They
22
+ * have to be passed in rather than looked up: a custom view is registered per
23
+ * collection, so there is no global set to check against, and validating
24
+ * against the built-ins alone is what would send `?__view=map` to the table.
25
+ *
26
+ * Omitting them checks the built-ins only, which is the right answer for
27
+ * callers that have no collection in hand.
28
+ */
29
+ export function isViewMode(value: string | null | undefined, customKeys?: readonly string[]): value is ViewMode {
30
+ if (!value) return false;
31
+ return VIEW_MODES.includes(value as ViewMode) || Boolean(customKeys?.includes(value));
19
32
  }
20
33
 
21
34
  /**
22
- * Read the active view mode from a query string, if it holds a valid one.
35
+ * Read the active view mode from a query string, if it holds one this
36
+ * collection can render.
23
37
  *
24
38
  * @param search a query string (e.g. `location.search`). Falls back to the
25
39
  * current browser location when omitted.
40
+ * @param customKeys keys of the collection's custom views, if any.
26
41
  */
27
- export function getViewModeFromSearch(search?: string): ViewMode | null {
42
+ export function getViewModeFromSearch(search?: string, customKeys?: readonly string[]): ViewMode | null {
28
43
  const source = search ?? (typeof window !== "undefined" ? window.location.search : "");
29
44
  const value = new URLSearchParams(source).get(VIEW_MODE_PARAM);
30
- return isViewMode(value) ? value : null;
45
+ return isViewMode(value, customKeys) ? value : null;
31
46
  }
32
47
 
33
48
  /**
@@ -42,7 +57,14 @@ export function getViewModeFromSearch(search?: string): ViewMode | null {
42
57
  * current browser location.
43
58
  */
44
59
  export function withViewMode(url: string, search?: string): string {
45
- const viewMode = getViewModeFromSearch(search);
60
+ // Carries whatever the param holds rather than validating it. These call
61
+ // sites are navigations — they have no collection in hand, so they cannot
62
+ // know whether `map` is one of *this* collection's custom views. Checking
63
+ // against the built-ins here would drop every custom view on the first
64
+ // record click. The binding validates on read, which is where the answer
65
+ // is knowable.
66
+ const source = search ?? (typeof window !== "undefined" ? window.location.search : "");
67
+ const viewMode = new URLSearchParams(source).get(VIEW_MODE_PARAM);
46
68
  if (!viewMode) return url;
47
69
 
48
70
  // Preserve any hash — the param belongs to the query, which precedes it.
@@ -101,16 +123,24 @@ export function withListState(url: string, search?: string): string {
101
123
  export function resolveViewMode({
102
124
  collection,
103
125
  search,
104
- savedViewMode
126
+ savedViewMode,
127
+ customKeys
105
128
  }: {
106
129
  collection?: Pick<AdminCollection<any>, "defaultViewMode">;
107
130
  search?: string;
108
131
  savedViewMode?: ViewMode | null;
132
+ /** Keys of the collection's resolved custom views. */
133
+ customKeys?: readonly string[];
109
134
  }): ViewMode {
110
- const fromUrl = getViewModeFromSearch(search);
135
+ const fromUrl = getViewModeFromSearch(search, customKeys);
111
136
  if (fromUrl) return fromUrl;
112
- if (isViewMode(savedViewMode)) return savedViewMode;
113
- return collection?.defaultViewMode ?? DEFAULT_VIEW_MODE;
137
+ if (isViewMode(savedViewMode, customKeys)) return savedViewMode;
138
+ // A `defaultViewMode` naming a custom view that is no longer registered
139
+ // would leave the collection on a mode nothing renders, so it gets the
140
+ // same check the other two sources get.
141
+ const configured = collection?.defaultViewMode;
142
+ if (isViewMode(configured, customKeys)) return configured;
143
+ return DEFAULT_VIEW_MODE;
114
144
  }
115
145
 
116
146
  /**
@@ -127,12 +157,19 @@ export function resolveViewMode({
127
157
  */
128
158
  export function resolveOpenEntityMode({
129
159
  collection,
130
- viewMode
160
+ viewMode,
161
+ customView
131
162
  }: {
132
163
  collection?: Pick<AdminCollection<any>, "openEntityMode">;
133
164
  viewMode?: ViewMode;
165
+ /** The resolved custom view, when `viewMode` names one. */
166
+ customView?: Pick<CollectionCustomView, "openEntityMode">;
134
167
  }): OpenEntityMode {
135
168
  if (collection?.openEntityMode) return collection.openEntityMode;
169
+ // A custom view gets to say, because only it knows whether it owns its
170
+ // surface. Falls to the board's answer: a map or a calendar keeps its
171
+ // canvas and shows the record over it, where "split" would halve it.
172
+ if (customView) return customView.openEntityMode ?? "side_panel";
136
173
  if (viewMode === "kanban") return "side_panel";
137
174
  if (viewMode === "table" || viewMode === "cards") return "full_screen";
138
175
  return DEFAULT_OPEN_ENTITY_MODE;