@rebasepro/admin 0.3.0 → 0.5.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 (106) hide show
  1. package/README.md +135 -0
  2. package/dist/{CollectionEditorDialog-D0VqpLPO.js → CollectionEditorDialog-C8E440oK.js} +22 -5
  3. package/dist/CollectionEditorDialog-C8E440oK.js.map +1 -0
  4. package/dist/{CollectionsStudioView-Bc3Rxxc2.js → CollectionsStudioView-Brr_b06v.js} +4 -4
  5. package/dist/{CollectionsStudioView-Bc3Rxxc2.js.map → CollectionsStudioView-Brr_b06v.js.map} +1 -1
  6. package/dist/{ExportCollectionAction-Ckc-09BQ.js → ExportCollectionAction-D3TTYXrq.js} +35 -27
  7. package/dist/ExportCollectionAction-D3TTYXrq.js.map +1 -0
  8. package/dist/{ImportCollectionAction-BqjIrC3Z.js → ImportCollectionAction-Dn7lya6E.js} +2 -2
  9. package/dist/{ImportCollectionAction-BqjIrC3Z.js.map → ImportCollectionAction-Dn7lya6E.js.map} +1 -1
  10. package/dist/{PropertyEditView-CvRSV-A2.js → PropertyEditView-ChBleIbN.js} +4 -4
  11. package/dist/PropertyEditView-ChBleIbN.js.map +1 -0
  12. package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +1 -0
  13. package/dist/collection_editor/ui/collection_editor/templates/blog_template.d.ts +1 -1
  14. package/dist/collection_editor/ui/collection_editor/templates/pages_template.d.ts +1 -1
  15. package/dist/collection_editor/ui/collection_editor/templates/products_template.d.ts +1 -1
  16. package/dist/collection_editor/ui/collection_editor/templates/users_template.d.ts +1 -1
  17. package/dist/collection_editor_ui.js +3 -3
  18. package/dist/components/CollectionPanel.d.ts +83 -0
  19. package/dist/components/EntityDetailView.d.ts +0 -3
  20. package/dist/components/EntityEditView.d.ts +2 -3
  21. package/dist/components/RebaseRouteDefs.d.ts +1 -1
  22. package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
  23. package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
  24. package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
  25. package/dist/components/admin/index.d.ts +1 -3
  26. package/dist/components/index.d.ts +1 -0
  27. package/dist/editor.js +15 -5
  28. package/dist/editor.js.map +1 -1
  29. package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
  30. package/dist/hooks/navigation/useResolvedViews.d.ts +2 -5
  31. package/dist/{index-BCcLwgfe.js → index-D3S9x_RP.js} +2 -2
  32. package/dist/{index-BCcLwgfe.js.map → index-D3S9x_RP.js.map} +1 -1
  33. package/dist/{index-DY2k5TtG.js → index-D9RNzm01.js} +3 -3
  34. package/dist/index-D9RNzm01.js.map +1 -0
  35. package/dist/{index-UQOMHwt1.js → index-DrrT332R.js} +3 -3
  36. package/dist/index-DrrT332R.js.map +1 -0
  37. package/dist/index.d.ts +2 -4
  38. package/dist/index.js +228 -295
  39. package/dist/index.js.map +1 -1
  40. package/dist/util/entity_view_constants.d.ts +6 -0
  41. package/dist/util/resolutions.d.ts +1 -2
  42. package/dist/{util-ZM9gQuCv.js → util-COYYD8zr.js} +473 -880
  43. package/dist/util-COYYD8zr.js.map +1 -0
  44. package/package.json +9 -10
  45. package/src/collection_editor/pgColumnToProperty.ts +19 -2
  46. package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +2 -2
  47. package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +2 -1
  48. package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +2 -2
  49. package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +2 -1
  50. package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +1 -1
  51. package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
  52. package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +1 -1
  53. package/src/collection_editor/ui/collection_editor/templates/products_template.ts +1 -1
  54. package/src/collection_editor/ui/collection_editor/templates/users_template.ts +1 -1
  55. package/src/collection_editor/validateCollectionJson.ts +12 -12
  56. package/src/components/CollectionPanel.tsx +155 -0
  57. package/src/components/DefaultDrawer.tsx +2 -2
  58. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +5 -2
  59. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +64 -34
  60. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +8 -1
  61. package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -1
  62. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
  63. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +68 -18
  64. package/src/components/EntityDetailView.tsx +32 -36
  65. package/src/components/EntityEditView.tsx +101 -50
  66. package/src/components/EntityEditViewFormActions.tsx +4 -4
  67. package/src/components/EntitySidePanel.tsx +50 -14
  68. package/src/components/PropertyCollectionView.tsx +1 -0
  69. package/src/components/RebaseRouteDefs.tsx +4 -6
  70. package/src/components/SelectableTable/SelectableTable.tsx +24 -22
  71. package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
  72. package/src/components/SelectableTable/SelectionStore.ts +4 -4
  73. package/src/components/admin/index.ts +1 -3
  74. package/src/components/index.ts +3 -3
  75. package/src/data_export/export/BasicExportAction.tsx +11 -9
  76. package/src/data_export/export/ExportCollectionAction.tsx +15 -13
  77. package/src/editor/editor.tsx +20 -4
  78. package/src/form/EntityForm.tsx +3 -3
  79. package/src/form/PropertyFieldBinding.tsx +1 -1
  80. package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
  81. package/src/form/field_bindings/MapFieldBinding.tsx +3 -45
  82. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
  83. package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -3
  84. package/src/hooks/navigation/useResolvedCollections.ts +2 -10
  85. package/src/hooks/navigation/useResolvedViews.tsx +6 -48
  86. package/src/hooks/useBuildSideEntityController.tsx +20 -3
  87. package/src/index.ts +6 -5
  88. package/src/util/entity_view_constants.ts +6 -0
  89. package/src/util/previews.ts +9 -1
  90. package/src/util/resolutions.ts +2 -6
  91. package/dist/CollectionEditorDialog-D0VqpLPO.js.map +0 -1
  92. package/dist/ExportCollectionAction-Ckc-09BQ.js.map +0 -1
  93. package/dist/PropertyEditView-CvRSV-A2.js.map +0 -1
  94. package/dist/components/admin/RoleChip.d.ts +0 -4
  95. package/dist/components/admin/RolesFilterSelect.d.ts +0 -2
  96. package/dist/components/admin/RolesView.d.ts +0 -4
  97. package/dist/components/admin/UserRolesSelectField.d.ts +0 -2
  98. package/dist/components/admin/UsersView.d.ts +0 -4
  99. package/dist/index-DY2k5TtG.js.map +0 -1
  100. package/dist/index-UQOMHwt1.js.map +0 -1
  101. package/dist/util-ZM9gQuCv.js.map +0 -1
  102. package/src/components/admin/RoleChip.tsx +0 -23
  103. package/src/components/admin/RolesFilterSelect.tsx +0 -45
  104. package/src/components/admin/RolesView.tsx +0 -470
  105. package/src/components/admin/UserRolesSelectField.tsx +0 -50
  106. package/src/components/admin/UsersView.tsx +0 -693
@@ -42,6 +42,7 @@ export function useCollectionInlineEditor<M extends Record<string, unknown>>({
42
42
  setError,
43
43
  data: entity
44
44
  }) => {
45
+ if (!entity) return;
45
46
 
46
47
  const updatedValues = setIn({}, propertyKey, value) as Partial<Record<string, unknown>>;
47
48
 
@@ -2,6 +2,7 @@ import { useMemo } from "react";
2
2
  import type { EntityCollection, Property, RelationProperty } from "@rebasepro/types";
3
3
  import type { Entity, PropertyConfig, EntityRelation } from "@rebasepro/types";
4
4
  import { getEntityImagePreviewPropertyKey } from "@rebasepro/common";
5
+ import { getEntityFromCache } from "@rebasepro/core";
5
6
  import { getEntityTitlePropertyKey, getEntityPreviewKeys } from "../../util/previews";
6
7
  import { getValueInPath } from "@rebasepro/utils";
7
8
  import type { AuthController } from "@rebasepro/types";
@@ -428,43 +429,92 @@ function resolveRelationDisplayName(
428
429
  ): string | undefined {
429
430
  // Support both EntityRelation instances and plain objects
430
431
  const data = "data" in relation ? (relation as EntityRelation).data : undefined;
431
- if (!data || !data.values) {
432
- // No eagerly-loaded data — fall back to ID
433
- const id = "id" in relation ? relation.id : undefined;
434
- return id !== undefined ? String(id) : undefined;
435
- }
436
-
437
- const values = data.values as Record<string, unknown>;
438
432
 
439
- // Try using the target collection's title property for accuracy
433
+ // Resolve target collection from either `prop.relation.target()` or `prop.target()` (inline API)
434
+ let targetCollection: EntityCollection | undefined;
440
435
  try {
441
- const targetCollection = prop.relation?.target?.();
436
+ const resolved = prop.relation?.target?.() ?? (typeof prop.target === "function" ? prop.target() : undefined);
437
+ if (resolved && typeof resolved === "object") {
438
+ targetCollection = resolved as EntityCollection;
439
+ }
440
+ } catch {
441
+ // Target collection may not be resolvable
442
+ }
443
+
444
+ // Helper: extract display name from entity values using the target collection
445
+ const extractDisplayName = (values: Record<string, unknown>): string | undefined => {
442
446
  if (targetCollection) {
443
447
  const targetTitleKey = targetCollection.titleProperty as string | undefined;
444
448
  if (targetTitleKey && values[targetTitleKey] !== undefined) {
445
449
  return String(values[targetTitleKey]);
446
450
  }
447
- // Fallback: find first non-id string property in target
451
+
452
+ // Helper to check if a property is hidden/internal
453
+ const isHiddenProp = (p: Property): boolean => {
454
+ if (p.ui?.hideFromCollection) return true;
455
+ if (typeof p.ui?.disabled === "object" && p.ui.disabled.hidden) return true;
456
+ return false;
457
+ };
458
+
459
+ // Helper to check if a property is a visible, non-id string
460
+ const isDisplayCandidate = (p: Property): boolean => {
461
+ return p.type === "string" && !p.multiline && !p.markdown && !p.storage
462
+ && !("isId" in p && p.isId) && !isHiddenProp(p);
463
+ };
464
+
465
+ // Prioritize common title-like fields: name, title, label, displayName
466
+ const priorityKeys = ["name", "title", "label", "displayName"];
467
+ for (const pk of priorityKeys) {
468
+ const p = targetCollection.properties[pk] as Property | undefined;
469
+ if (p && isDisplayCandidate(p) && values[pk] !== undefined && values[pk] !== null) {
470
+ return String(values[pk]);
471
+ }
472
+ }
473
+
474
+ // Fallback: find first visible, non-id string property in target
448
475
  for (const [k, p] of Object.entries(targetCollection.properties)) {
449
476
  const tp = p as Property;
450
- if (tp.type === "string" && !tp.multiline && !tp.markdown && !tp.storage && !("isId" in tp && tp.isId)) {
477
+ if (isDisplayCandidate(tp)) {
451
478
  if (values[k] !== undefined && values[k] !== null) {
452
479
  return String(values[k]);
453
480
  }
454
481
  }
455
482
  }
456
483
  }
457
- } catch {
458
- // Target collection may not be resolvable — fall through
484
+ // Generic fallback: walk entity values for any short string.
485
+ for (const [, v] of Object.entries(values)) {
486
+ if (typeof v === "string" && v.length > 0 && v.length < 200) {
487
+ return v;
488
+ }
489
+ }
490
+ return undefined;
491
+ };
492
+
493
+ // 1. Try eagerly-loaded data on the relation object
494
+ if (data && data.values) {
495
+ const result = extractDisplayName(data.values as Record<string, unknown>);
496
+ if (result) return result;
459
497
  }
460
498
 
461
- // Generic fallback: walk entity values for any short string.
462
- for (const [, v] of Object.entries(values)) {
463
- if (typeof v === "string" && v.length > 0 && v.length < 200) {
464
- return v;
499
+ const id = "id" in relation ? relation.id : undefined;
500
+
501
+ // 2. Try the entity cache (sessionStorage) as a fallback
502
+ if (id !== undefined && targetCollection) {
503
+ try {
504
+ const slug = targetCollection.slug ?? ("table" in targetCollection ? targetCollection.table : undefined);
505
+ if (slug) {
506
+ const cacheKey = `${slug}/${id}`;
507
+ const cached = getEntityFromCache(cacheKey) as { values?: Record<string, unknown> } | undefined;
508
+ if (cached?.values) {
509
+ const result = extractDisplayName(cached.values);
510
+ if (result) return result;
511
+ }
512
+ }
513
+ } catch {
514
+ // Cache lookup failed — fall through
465
515
  }
466
516
  }
467
517
 
468
- const id = "id" in relation ? relation.id : undefined;
518
+ // 3. Final fallback: show the ID
469
519
  return id !== undefined ? String(id) : undefined;
470
520
  }
@@ -1,4 +1,4 @@
1
- import type { ComponentRef, EntityCollection, EntityCustomViewParams, EntityDetailViewConfig } from "@rebasepro/types";
1
+ import type { ComponentRef, EntityCollection, EntityCustomViewParams, FormViewConfig } from "@rebasepro/types";
2
2
  import type { FormContext } from "../types/fields";
3
3
  import type { PluginFormActionProps } from "@rebasepro/types";
4
4
  import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -47,9 +47,7 @@ import { EntityJsonPreview } from "../components/EntityJsonPreview";
47
47
 
48
48
  const EntityHistoryView = lazy(() => import("../components/history").then(m => ({ default: m.EntityHistoryView })));
49
49
 
50
- export const MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
51
- export const JSON_TAB_VALUE = "__json";
52
- export const HISTORY_TAB_VALUE = "__rebase_history";
50
+ import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/entity_view_constants";
53
51
 
54
52
  export type BarActionsParams = {
55
53
  values: object,
@@ -154,9 +152,19 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
154
152
  { status: "existing", entityId }
155
153
  ), [collection, entityId]);
156
154
 
157
- const [selectedTab, setSelectedTab] = useState<string>(selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
155
+ // Track whether the user has explicitly clicked a tab in this component
156
+ // instance. See EntityEditViewInner for full explanation.
157
+ const userHasChangedTab = useRef(false);
158
+
159
+ const [selectedTab, setSelectedTab] = useState<string>(() => {
160
+ const val = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
161
+ return val === "edit" ? MAIN_TAB_VALUE : val;
162
+ });
158
163
  useEffect(() => {
159
- const target = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
164
+ const val = userHasChangedTab.current
165
+ ? (selectedTabProp ?? MAIN_TAB_VALUE)
166
+ : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
167
+ const target = val === "edit" ? MAIN_TAB_VALUE : val;
160
168
  if (target !== selectedTab) {
161
169
  setSelectedTab(target);
162
170
  }
@@ -223,8 +231,9 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
223
231
  }), [entityId, parentCollectionSlugs, parentEntityIds, path, collection, context, readOnlyFormContext, layout]);
224
232
  const pluginActionsTop = useSlot("form.actions.top", formActionTopProps);
225
233
 
226
- // Detail view config from collection
227
- const detailViewConfig = (collection as EntityCollection<M> & { detailView?: EntityDetailViewConfig<M> }).detailView;
234
+ // Resolve formView.Builder if provided
235
+ const formViewConfig = (collection as EntityCollection<M> & { formView?: FormViewConfig<M> }).formView;
236
+ const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
228
237
 
229
238
  // Title resolution
230
239
  const titlePropertyKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
@@ -334,6 +343,7 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
334
343
  }).filter(Boolean);
335
344
 
336
345
  const onSideTabClick = useCallback((value: string) => {
346
+ userHasChangedTab.current = true;
337
347
  setSelectedTab(value);
338
348
  onTabChange?.({
339
349
  path,
@@ -344,43 +354,29 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
344
354
  }, [onTabChange, path, entityId, collection]);
345
355
 
346
356
  const propertyDetailView = () => {
347
- // Allow full override via detailView.Builder
348
- if (detailViewConfig?.Builder) {
349
- const CustomBuilder = resolveComponentRef(detailViewConfig.Builder);
350
- if (CustomBuilder && usedEntity) {
351
- return <CustomBuilder
352
- collection={collection}
353
- entity={usedEntity}
354
- path={path}
355
- onEditClick={onEditClick ?? (() => {})}
356
- />;
357
- }
357
+ // formView.Builder replaces the default property display
358
+ if (FormViewBuilder && usedEntity) {
359
+ return <ErrorBoundary>
360
+ <Suspense fallback={<CircularProgressCenter />}>
361
+ <FormViewBuilder
362
+ collection={collection}
363
+ parentCollectionSlugs={parentCollectionSlugs}
364
+ parentEntityIds={parentEntityIds}
365
+ entity={usedEntity}
366
+ modifiedValues={usedEntity?.values}
367
+ formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
368
+ />
369
+ </Suspense>
370
+ </ErrorBoundary>;
358
371
  }
359
372
 
360
- const HeaderComponent = detailViewConfig?.Header ? resolveComponentRef(detailViewConfig.Header) : null;
361
- const FooterComponent = detailViewConfig?.Footer ? resolveComponentRef(detailViewConfig.Footer) : null;
362
-
363
373
  return (
364
374
  <>
365
- {HeaderComponent && usedEntity && <HeaderComponent
366
- collection={collection}
367
- entity={usedEntity}
368
- path={path}
369
- onEditClick={onEditClick ?? (() => {})}
370
- />}
371
-
372
375
  {usedEntity && <EntityView
373
376
  entity={usedEntity}
374
377
  collection={collection}
375
378
  path={path}
376
379
  />}
377
-
378
- {FooterComponent && usedEntity && <FooterComponent
379
- collection={collection}
380
- entity={usedEntity}
381
- path={path}
382
- onEditClick={onEditClick ?? (() => {})}
383
- />}
384
380
  </>
385
381
  );
386
382
  };
@@ -1,4 +1,4 @@
1
- import type { ComponentRef, EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
1
+ import type { ComponentRef, EntityCollection, EntityCustomViewParams, FormViewConfig } from "@rebasepro/types";
2
2
  import type { FormContext } from "../types/fields";
3
3
  import type { PluginFormActionProps } from "@rebasepro/types";
4
4
  import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -50,9 +50,8 @@ import { usePermissions } from "@rebasepro/core";
50
50
  import { useUrlController } from "../index";
51
51
  import { useNavigate } from "react-router-dom";
52
52
 
53
- export const MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
54
- export const JSON_TAB_VALUE = "__json";
55
- export const HISTORY_TAB_VALUE = "__rebase_history";
53
+ import { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE } from "../util/entity_view_constants";
54
+ export { MAIN_TAB_VALUE, JSON_TAB_VALUE, HISTORY_TAB_VALUE };
56
55
 
57
56
  export type BarActionsParams = {
58
57
  values: object,
@@ -225,11 +224,22 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
225
224
  }
226
225
  ), [collection, status, entityId]);
227
226
 
228
- const [selectedTab, setSelectedTab] = useState<string>(
229
- selectedTabProp === "edit" ? MAIN_TAB_VALUE : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE)
230
- );
227
+ // Track whether the user has explicitly clicked a tab in this component
228
+ // instance. When false, we fall back to defaultSelectedView (which may
229
+ // resolve asynchronously if the collection loads from the registry after
230
+ // mount). Once true, selectedTabProp=undefined means the user selected
231
+ // the form tab, so we must NOT re-apply defaultSelectedView.
232
+ const userHasChangedTab = useRef(false);
233
+
234
+ const [selectedTab, setSelectedTab] = useState<string>(() => {
235
+ const val = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
236
+ return val === "edit" ? MAIN_TAB_VALUE : val;
237
+ });
231
238
  useEffect(() => {
232
- const target = selectedTabProp === "edit" ? MAIN_TAB_VALUE : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
239
+ const val = userHasChangedTab.current
240
+ ? (selectedTabProp ?? MAIN_TAB_VALUE)
241
+ : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
242
+ const target = val === "edit" ? MAIN_TAB_VALUE : val;
233
243
  if (target !== selectedTab) {
234
244
  setSelectedTab(target);
235
245
  }
@@ -268,6 +278,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
268
278
  return {
269
279
  entityId,
270
280
  disabled: false,
281
+ readOnly: true,
271
282
  openEntityMode: layout,
272
283
  status: status,
273
284
  values: usedEntity?.values ?? ({} as M),
@@ -402,6 +413,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
402
413
  }).filter(Boolean);
403
414
 
404
415
  const onSideTabClick = useCallback((value: string) => {
416
+ userHasChangedTab.current = true;
405
417
  setSelectedTab(value);
406
418
  if (status === "existing") {
407
419
  onTabChange?.({
@@ -413,6 +425,11 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
413
425
  }
414
426
  }, [status, onTabChange, path, entityId, collection]);
415
427
 
428
+ // Resolve formView.Builder if provided
429
+ const formViewConfig = (collection as EntityCollection<M> & { formView?: FormViewConfig<M> }).formView;
430
+ const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
431
+ const formViewIncludeActions = formViewConfig?.includeActions !== false;
432
+
416
433
  const entityReadOnlyView = !canEdit && entity ? <div
417
434
  className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", (canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}>
418
435
  <div
@@ -420,52 +437,86 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
420
437
  <Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
421
438
  {collection.singularName ?? collection.name}
422
439
  </Typography>
423
- <EntityView
424
- className={"px-8 h-full overflow-auto"}
425
- entity={entity}
426
- path={path}
427
- collection={collection} />
440
+ {FormViewBuilder && readOnlyFormContext ? (
441
+ <ErrorBoundary>
442
+ <Suspense fallback={<CircularProgressCenter />}>
443
+ <FormViewBuilder
444
+ collection={collection}
445
+ parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
446
+ entity={usedEntity}
447
+ modifiedValues={usedEntity?.values}
448
+ formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
449
+ />
450
+ </Suspense>
451
+ </ErrorBoundary>
452
+ ) : (
453
+ <EntityView
454
+ className={"px-8 h-full overflow-auto"}
455
+ entity={entity}
456
+ path={path}
457
+ collection={collection} />
458
+ )}
428
459
  <div className="h-16" />
429
460
  </div>
430
461
  </div> : null;
431
462
 
432
- const entityView = <EntityForm<M>
433
- collection={collection}
434
- path={path}
435
- entityId={entityId ?? usedEntity?.id}
436
- onValuesModified={onValuesModified}
437
- entity={entity}
438
- initialDirtyValues={initialDirtyValues}
439
- openEntityMode={layout}
440
- forceActionsAtTheBottom={actionsAtTheBottom}
441
- initialStatus={status}
442
- className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
443
- EntityFormActionsComponent={EntityEditViewFormActions}
444
- disabled={!canEdit}
445
- navigateBack={navigateBack}
446
- {...formProps}
447
- onEntityChange={(entity) => {
448
- setUsedEntity(entity);
449
- formProps?.onEntityChange?.(entity);
450
- }}
451
- onStatusChange={(status) => {
452
- setStatus(status);
453
- formProps?.onStatusChange?.(status);
454
- }}
455
- onFormContextReady={(formContext) => {
456
- setFormContext(formContext);
457
- formProps?.onFormContextReady?.(formContext);
458
- }}
459
- onSaved={(params) => {
460
- const res = {
461
- ...params,
462
- selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
463
- };
464
- onSaved?.(res);
465
- formProps?.onSaved?.(res);
466
- }}
467
- Builder={resolveComponentRef(selectedSecondaryForm?.Builder as ComponentRef<EntityCustomViewParams<M>> | undefined) as React.ComponentType<EntityCustomViewParams<M>> | undefined}
468
- />;
463
+ const entityView = FormViewBuilder ? (
464
+ // formView.Builder replaces the default form
465
+ <div className={cls(
466
+ "relative flex-1 w-full h-full overflow-auto",
467
+ (!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : ""
468
+ )}>
469
+ <ErrorBoundary>
470
+ <Suspense fallback={<CircularProgressCenter />}>
471
+ {(formContext ?? readOnlyFormContext) && <FormViewBuilder
472
+ collection={collection}
473
+ parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
474
+ entity={usedEntity}
475
+ modifiedValues={(formContext ?? readOnlyFormContext)?.formex?.values ?? usedEntity?.values}
476
+ formContext={(formContext ?? readOnlyFormContext) as FormContext<Record<string, unknown>>}
477
+ />}
478
+ </Suspense>
479
+ </ErrorBoundary>
480
+ </div>
481
+ ) : (
482
+ <EntityForm<M>
483
+ collection={collection}
484
+ path={path}
485
+ entityId={entityId ?? usedEntity?.id}
486
+ onValuesModified={onValuesModified}
487
+ entity={entity}
488
+ initialDirtyValues={initialDirtyValues}
489
+ openEntityMode={layout}
490
+ forceActionsAtTheBottom={actionsAtTheBottom}
491
+ initialStatus={status}
492
+ className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
493
+ EntityFormActionsComponent={EntityEditViewFormActions}
494
+ disabled={!canEdit}
495
+ navigateBack={navigateBack}
496
+ {...formProps}
497
+ onEntityChange={(entity) => {
498
+ setUsedEntity(entity);
499
+ formProps?.onEntityChange?.(entity);
500
+ }}
501
+ onStatusChange={(status) => {
502
+ setStatus(status);
503
+ formProps?.onStatusChange?.(status);
504
+ }}
505
+ onFormContextReady={(formContext) => {
506
+ setFormContext(formContext);
507
+ formProps?.onFormContextReady?.(formContext);
508
+ }}
509
+ onSaved={(params) => {
510
+ const res = {
511
+ ...params,
512
+ selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
513
+ };
514
+ onSaved?.(res);
515
+ formProps?.onSaved?.(res);
516
+ }}
517
+ Builder={resolveComponentRef(selectedSecondaryForm?.Builder as ComponentRef<EntityCustomViewParams<M>> | undefined) as React.ComponentType<EntityCustomViewParams<M>> | undefined}
518
+ />
519
+ );
469
520
 
470
521
  const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
471
522
  const icon = getIcon(subcollection.icon, undefined, undefined, "small");
@@ -162,7 +162,7 @@ function buildBottomActions<M extends Record<string, unknown>>({
162
162
  }
163
163
 
164
164
  {formActions.length > 0 && <div className="grow flex overflow-auto no-scrollbar">
165
- {formActions.map(action => {
165
+ {formActions.map((action, index) => {
166
166
 
167
167
  const props = {
168
168
  view: "form",
@@ -179,7 +179,7 @@ function buildBottomActions<M extends Record<string, unknown>>({
179
179
  const isEnabled = !action.isEnabled || action.isEnabled(props);
180
180
  return (
181
181
  <EntityActionButton
182
- key={action.key}
182
+ key={action.key ?? action.name ?? index}
183
183
  action={action}
184
184
  enabled={isEnabled}
185
185
  props={props}
@@ -282,7 +282,7 @@ function buildSideActions<M extends Record<string, unknown>>({
282
282
  {pluginActions}
283
283
 
284
284
  {formActions.length > 0 && <div className="flex flex-row flex-wrap mt-2">
285
- {formActions.map(action => {
285
+ {formActions.map((action, index) => {
286
286
  const props = {
287
287
  view: "form",
288
288
  entity,
@@ -296,7 +296,7 @@ function buildSideActions<M extends Record<string, unknown>>({
296
296
  } satisfies EntityActionClickProps<any>;
297
297
  const isEnabled = !action.isEnabled || action.isEnabled(props);
298
298
  return (
299
- <EntityActionButton key={action.key} action={action} enabled={isEnabled} props={props}/>
299
+ <EntityActionButton key={action.key ?? action.name ?? index} action={action} enabled={isEnabled} props={props}/>
300
300
  );
301
301
  })}
302
302
  </div>}
@@ -1,6 +1,6 @@
1
1
  import type { EntityCollection } from "@rebasepro/types";
2
2
  import type { EntitySidePanelProps } from "@rebasepro/types";
3
- import React, { useCallback, useEffect, useMemo, useState } from "react";
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
 
5
5
  import type { OnUpdateParams } from "../types/components/EntityFormProps";
6
6
  import { ErrorBoundary } from "@rebasepro/ui";
@@ -8,11 +8,11 @@ import { IconButton, Maximize2Icon, XIcon } from "@rebasepro/ui";
8
8
  import { EntityEditView } from "./EntityEditView";
9
9
  import { EntityDetailView } from "./EntityDetailView";
10
10
  import { useSideDialogContext } from "./SideDialogs";
11
- import { useLocation, useNavigate } from "react-router-dom";
12
- import { removeInitialAndTrailingSlashes } from "@rebasepro/common";
11
+ import { useNavigate } from "react-router-dom";
13
12
  import { saveEntityToMemoryCache } from "@rebasepro/core";
14
13
  import { useCollectionRegistryController, useSideEntityController } from "../index";
15
14
  import { useUrlController } from "../index";
15
+ import { resolveDefaultSelectedView } from "@rebasepro/common";
16
16
 
17
17
  /**
18
18
  * This is the component in charge of rendering the side dialog used
@@ -40,7 +40,6 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
40
40
  } = useSideDialogContext();
41
41
 
42
42
  const navigate = useNavigate();
43
- const location = useLocation();
44
43
 
45
44
  const sideEntityController = useSideEntityController();
46
45
  const collectionRegistryController = useCollectionRegistryController();
@@ -96,6 +95,37 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
96
95
  setShowEditInPanel(selectedTab === "edit");
97
96
  }, [entityId, selectedTab]);
98
97
 
98
+ // One-time correction: when the side panel opens without the correct
99
+ // selectedTab but the resolved collection (from the registry) has a
100
+ // defaultSelectedView, update the panel width and URL to match.
101
+ // This handles cases where defaultSelectedView is set via the collection
102
+ // editor / Studio and is not available on the collection object the
103
+ // caller passed to sideEntityController.open().
104
+ const hasCorrectedDefaultView = useRef(false);
105
+ useEffect(() => {
106
+ hasCorrectedDefaultView.current = false;
107
+ }, [entityId]);
108
+ useEffect(() => {
109
+ if (hasCorrectedDefaultView.current) return;
110
+ if (selectedTab) return; // Already has a tab — no correction needed
111
+ if (!entityId || !collection?.defaultSelectedView) return;
112
+
113
+ const effectiveDefault = resolveDefaultSelectedView(
114
+ collection.defaultSelectedView,
115
+ { status: "existing", entityId }
116
+ );
117
+ if (effectiveDefault && effectiveDefault !== "edit") {
118
+ hasCorrectedDefaultView.current = true;
119
+ sideEntityController.replace({
120
+ path,
121
+ entityId,
122
+ selectedTab: effectiveDefault,
123
+ updateUrl: collection.openEntityMode !== "dialog",
124
+ collection
125
+ });
126
+ }
127
+ }, [selectedTab, entityId, collection, path, sideEntityController]);
128
+
99
129
  // Note: beforeunload is handled by useUnsavedChangesDialog in SideDialogView,
100
130
  // which listens to the same `blocked` state via SideDialogContext.
101
131
 
@@ -153,10 +183,13 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
153
183
  return;
154
184
  }
155
185
  if (tabEntityId) {
156
- const collectionPath = removeInitialAndTrailingSlashes(path);
157
- const tabSuffix = selectedTab ? "/" + selectedTab : "";
158
- const fullUrl = urlController.buildUrlCollectionPath(`${collectionPath}/${tabEntityId}${tabSuffix}#side`);
159
- navigate(fullUrl, { replace: true, state: location.state });
186
+ sideEntityController.replace({
187
+ path,
188
+ entityId: tabEntityId,
189
+ selectedTab,
190
+ updateUrl: true,
191
+ collection: paramCollection ?? collection
192
+ });
160
193
  }
161
194
  }}
162
195
  />
@@ -198,18 +231,21 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
198
231
  </IconButton>}
199
232
  </div>}
200
233
  onTabChange={({
201
- entityId,
234
+ entityId: tabEntityId,
202
235
  selectedTab,
203
236
  collection: paramCollection
204
237
  }) => {
205
238
  if (collection?.openEntityMode === "dialog" || paramCollection?.openEntityMode === "dialog") {
206
239
  return;
207
240
  }
208
- if (entityId) {
209
- const collectionPath = removeInitialAndTrailingSlashes(path);
210
- const tabSuffix = selectedTab ? "/" + selectedTab : "";
211
- const fullUrl = urlController.buildUrlCollectionPath(`${collectionPath}/${entityId}${tabSuffix}#side`);
212
- navigate(fullUrl, { replace: true, state: location.state });
241
+ if (tabEntityId) {
242
+ sideEntityController.replace({
243
+ path,
244
+ entityId: tabEntityId,
245
+ selectedTab,
246
+ updateUrl: true,
247
+ collection: paramCollection ?? collection
248
+ });
213
249
  }
214
250
  }}
215
251
  formProps={formProps}
@@ -258,6 +258,7 @@ export const PropertyCollectionView = ({
258
258
  <div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
259
259
  {Object.entries(data).map(([key, value]) => {
260
260
  if (value === null || value === undefined) return null;
261
+ if (baseKey === "" && !properties[key]) return null;
261
262
  const currentKey = baseKey ? `${baseKey}.${key}` : key;
262
263
  return (
263
264
  <PropertyCollectionView
@@ -5,13 +5,12 @@ import {
5
5
  RebaseRoutes,
6
6
  UserSettingsView,
7
7
  UIReferenceView,
8
- NotFoundPage,
9
- useInternalUserManagementController
8
+ NotFoundPage
10
9
  } from "@rebasepro/core";
11
10
  import { CircularProgressCenter } from "@rebasepro/ui";
12
11
 
13
12
  import { ContentHomePage } from "./HomePage/ContentHomePage";
14
- import { RolesView } from "./admin/RolesView";
13
+
15
14
  import { RebaseRoute } from "../routes/RebaseRoute";
16
15
  import { CustomViewRoute } from "../routes/CustomViewRoute";
17
16
  import { useNavigationStateController } from "../hooks/navigation/contexts/NavigationStateContext";
@@ -48,7 +47,7 @@ export interface RebaseRouteDefsProps {
48
47
  * Route definitions for the CMS.
49
48
  *
50
49
  * Defines all standard routes: home, studio home, collection view,
51
- * settings, users, roles, debug, custom views, and a catch-all 404.
50
+ * settings, debug, custom views, and a catch-all 404.
52
51
  *
53
52
  * **Independently usable**: Use this when you want Rebase routes
54
53
  * inside your own layout/navigation setup.
@@ -64,7 +63,6 @@ export interface RebaseRouteDefsProps {
64
63
  */
65
64
  export function RebaseRouteDefs({ children, layout }: RebaseRouteDefsProps) {
66
65
  const registry = useRebaseRegistry();
67
- const userManagement = useInternalUserManagementController();
68
66
  const navigationStateController = useNavigationStateController();
69
67
 
70
68
  const cmsHomePage = registry.cmsConfig?.homePage ?? <Suspense fallback={<CircularProgressCenter/>}><ContentHomePage/></Suspense>;
@@ -85,7 +83,7 @@ export function RebaseRouteDefs({ children, layout }: RebaseRouteDefsProps) {
85
83
 
86
84
  <Route path={"/c/*"} element={<RebaseRoute/>}/>
87
85
  <Route path={"/settings"} element={<SettingsView/>}/>
88
- {userManagement && <Route path={"/roles"} element={<RolesView userManagement={userManagement}/>}/>}
86
+
89
87
 
90
88
  {/* Hidden debug route */}
91
89
  <Route path={"/debug/ui"} element={<UIReferenceView/>}/>