@rebasepro/admin 0.2.5 → 0.4.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 (64) hide show
  1. package/dist/{CollectionEditorDialog-Cn8-tGyL.js → CollectionEditorDialog-Bo86h-d-.js} +3 -3
  2. package/dist/{CollectionEditorDialog-Cn8-tGyL.js.map → CollectionEditorDialog-Bo86h-d-.js.map} +1 -1
  3. package/dist/{CollectionsStudioView-C-Ts1rZt.js → CollectionsStudioView-bsg4AT_Z.js} +4 -4
  4. package/dist/{CollectionsStudioView-C-Ts1rZt.js.map → CollectionsStudioView-bsg4AT_Z.js.map} +1 -1
  5. package/dist/{ExportCollectionAction-BRdKM3DF.js → ExportCollectionAction-Cf-Id5uO.js} +35 -27
  6. package/dist/ExportCollectionAction-Cf-Id5uO.js.map +1 -0
  7. package/dist/{ImportCollectionAction-U-v7lGxO.js → ImportCollectionAction-LN5sreK9.js} +2 -2
  8. package/dist/{ImportCollectionAction-U-v7lGxO.js.map → ImportCollectionAction-LN5sreK9.js.map} +1 -1
  9. package/dist/{PropertyEditView-BDNYkfNf.js → PropertyEditView-jCWCxmCz.js} +2 -2
  10. package/dist/{PropertyEditView-BDNYkfNf.js.map → PropertyEditView-jCWCxmCz.js.map} +1 -1
  11. package/dist/collection_editor_ui.js +3 -3
  12. package/dist/components/CollectionPanel.d.ts +83 -0
  13. package/dist/components/EntityDetailView.d.ts +0 -3
  14. package/dist/components/EntityEditView.d.ts +2 -3
  15. package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
  16. package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
  17. package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/editor.js +15 -5
  20. package/dist/editor.js.map +1 -1
  21. package/dist/{index-DJSL_SCr.js → index-BrqSvMG0.js} +3 -3
  22. package/dist/index-BrqSvMG0.js.map +1 -0
  23. package/dist/{index-DHaOV-7A.js → index-Cb0bPnzZ.js} +3 -3
  24. package/dist/index-Cb0bPnzZ.js.map +1 -0
  25. package/dist/{index-XMII4H3d.js → index-z3H8iqsj.js} +2 -2
  26. package/dist/{index-XMII4H3d.js.map → index-z3H8iqsj.js.map} +1 -1
  27. package/dist/index.d.ts +2 -2
  28. package/dist/index.js +142 -4
  29. package/dist/index.js.map +1 -1
  30. package/dist/util/entity_view_constants.d.ts +6 -0
  31. package/dist/util/resolutions.d.ts +1 -2
  32. package/dist/{util-0GYaJqL_.js → util-CvNp6A-r.js} +283 -168
  33. package/dist/util-CvNp6A-r.js.map +1 -0
  34. package/package.json +8 -8
  35. package/src/collection_editor/validateCollectionJson.ts +12 -12
  36. package/src/components/CollectionPanel.tsx +155 -0
  37. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +65 -35
  38. package/src/components/EntityCollectionView/EntityCollectionListView.tsx +1 -1
  39. package/src/components/EntityCollectionView/EntityCollectionView.tsx +1 -1
  40. package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
  41. package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +69 -18
  42. package/src/components/EntityDetailView.tsx +36 -37
  43. package/src/components/EntityEditView.tsx +108 -51
  44. package/src/components/EntityEditViewFormActions.tsx +4 -4
  45. package/src/components/EntitySidePanel.tsx +47 -14
  46. package/src/components/PropertyCollectionView.tsx +1 -0
  47. package/src/components/SelectableTable/SelectableTable.tsx +24 -22
  48. package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
  49. package/src/components/SelectableTable/SelectionStore.ts +4 -4
  50. package/src/components/index.ts +2 -0
  51. package/src/data_export/export/BasicExportAction.tsx +11 -9
  52. package/src/data_export/export/ExportCollectionAction.tsx +15 -13
  53. package/src/editor/editor.tsx +20 -4
  54. package/src/form/EntityForm.tsx +3 -3
  55. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
  56. package/src/hooks/navigation/useResolvedCollections.ts +2 -10
  57. package/src/hooks/useBuildSideEntityController.tsx +20 -3
  58. package/src/index.ts +4 -2
  59. package/src/util/entity_view_constants.ts +6 -0
  60. package/src/util/resolutions.ts +2 -6
  61. package/dist/ExportCollectionAction-BRdKM3DF.js.map +0 -1
  62. package/dist/index-DHaOV-7A.js.map +0 -1
  63. package/dist/index-DJSL_SCr.js.map +0 -1
  64. package/dist/util-0GYaJqL_.js.map +0 -1
@@ -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,13 +152,27 @@ 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 we've applied the defaultSelectedView already.
156
+ // See EntityEditView for a full explanation of this pattern.
157
+ const hasAppliedDefault = useRef(false);
158
+
159
+ const [selectedTab, setSelectedTab] = useState<string>(() => {
160
+ const val = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
161
+ hasAppliedDefault.current = true;
162
+ return val === "edit" ? MAIN_TAB_VALUE : val;
163
+ });
158
164
  useEffect(() => {
159
- const target = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
165
+ const val = hasAppliedDefault.current
166
+ ? (selectedTabProp ?? MAIN_TAB_VALUE)
167
+ : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
168
+ const target = val === "edit" ? MAIN_TAB_VALUE : val;
169
+ if (!hasAppliedDefault.current) {
170
+ hasAppliedDefault.current = true;
171
+ }
160
172
  if (target !== selectedTab) {
161
173
  setSelectedTab(target);
162
174
  }
163
- }, [selectedTabProp, defaultSelectedView]);
175
+ }, [selectedTabProp]);
164
176
 
165
177
  const subcollections = getSubcollections(collection).filter(c => !c.hideFromNavigation);
166
178
  const subcollectionsCount = subcollections?.length ?? 0;
@@ -223,8 +235,9 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
223
235
  }), [entityId, parentCollectionSlugs, parentEntityIds, path, collection, context, readOnlyFormContext, layout]);
224
236
  const pluginActionsTop = useSlot("form.actions.top", formActionTopProps);
225
237
 
226
- // Detail view config from collection
227
- const detailViewConfig = (collection as EntityCollection<M> & { detailView?: EntityDetailViewConfig<M> }).detailView;
238
+ // Resolve formView.Builder if provided
239
+ const formViewConfig = (collection as EntityCollection<M> & { formView?: FormViewConfig<M> }).formView;
240
+ const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
228
241
 
229
242
  // Title resolution
230
243
  const titlePropertyKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
@@ -344,43 +357,29 @@ function EntityDetailViewInner<M extends Record<string, unknown>>({
344
357
  }, [onTabChange, path, entityId, collection]);
345
358
 
346
359
  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
- }
360
+ // formView.Builder replaces the default property display
361
+ if (FormViewBuilder && usedEntity) {
362
+ return <ErrorBoundary>
363
+ <Suspense fallback={<CircularProgressCenter />}>
364
+ <FormViewBuilder
365
+ collection={collection}
366
+ parentCollectionSlugs={parentCollectionSlugs}
367
+ parentEntityIds={parentEntityIds}
368
+ entity={usedEntity}
369
+ modifiedValues={usedEntity?.values}
370
+ formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
371
+ />
372
+ </Suspense>
373
+ </ErrorBoundary>;
358
374
  }
359
375
 
360
- const HeaderComponent = detailViewConfig?.Header ? resolveComponentRef(detailViewConfig.Header) : null;
361
- const FooterComponent = detailViewConfig?.Footer ? resolveComponentRef(detailViewConfig.Footer) : null;
362
-
363
376
  return (
364
377
  <>
365
- {HeaderComponent && usedEntity && <HeaderComponent
366
- collection={collection}
367
- entity={usedEntity}
368
- path={path}
369
- onEditClick={onEditClick ?? (() => {})}
370
- />}
371
-
372
378
  {usedEntity && <EntityView
373
379
  entity={usedEntity}
374
380
  collection={collection}
375
381
  path={path}
376
382
  />}
377
-
378
- {FooterComponent && usedEntity && <FooterComponent
379
- collection={collection}
380
- entity={usedEntity}
381
- path={path}
382
- onEditClick={onEditClick ?? (() => {})}
383
- />}
384
383
  </>
385
384
  );
386
385
  };
@@ -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,15 +224,33 @@ 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 we've applied the defaultSelectedView already.
228
+ // On the initial render, selectedTabProp may be undefined because the URL
229
+ // has no tab segment — in that case we fall back to defaultSelectedView.
230
+ // But once the component is mounted, selectedTabProp=undefined means the
231
+ // user explicitly selected the form tab (which has no URL segment), so we
232
+ // must NOT re-apply defaultSelectedView.
233
+ const hasAppliedDefault = useRef(false);
234
+
235
+ const [selectedTab, setSelectedTab] = useState<string>(() => {
236
+ const val = selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE;
237
+ hasAppliedDefault.current = true;
238
+ return val === "edit" ? MAIN_TAB_VALUE : val;
239
+ });
231
240
  useEffect(() => {
232
- const target = selectedTabProp === "edit" ? MAIN_TAB_VALUE : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
241
+ // After initial mount, only sync from selectedTabProp don't fall
242
+ // back to defaultSelectedView, because undefined now means "form tab".
243
+ const val = hasAppliedDefault.current
244
+ ? (selectedTabProp ?? MAIN_TAB_VALUE)
245
+ : (selectedTabProp ?? defaultSelectedView ?? MAIN_TAB_VALUE);
246
+ const target = val === "edit" ? MAIN_TAB_VALUE : val;
247
+ if (!hasAppliedDefault.current) {
248
+ hasAppliedDefault.current = true;
249
+ }
233
250
  if (target !== selectedTab) {
234
251
  setSelectedTab(target);
235
252
  }
236
- }, [selectedTabProp, defaultSelectedView]);
253
+ }, [selectedTabProp]);
237
254
 
238
255
  const subcollections = getSubcollections(collection).filter(c => !c.hideFromNavigation);
239
256
  const subcollectionsCount = subcollections?.length ?? 0;
@@ -268,6 +285,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
268
285
  return {
269
286
  entityId,
270
287
  disabled: false,
288
+ readOnly: true,
271
289
  openEntityMode: layout,
272
290
  status: status,
273
291
  values: usedEntity?.values ?? ({} as M),
@@ -413,6 +431,11 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
413
431
  }
414
432
  }, [status, onTabChange, path, entityId, collection]);
415
433
 
434
+ // Resolve formView.Builder if provided
435
+ const formViewConfig = (collection as EntityCollection<M> & { formView?: FormViewConfig<M> }).formView;
436
+ const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef<EntityCustomViewParams>(formViewConfig.Builder as ComponentRef<EntityCustomViewParams>) : null;
437
+ const formViewIncludeActions = formViewConfig?.includeActions !== false;
438
+
416
439
  const entityReadOnlyView = !canEdit && entity ? <div
417
440
  className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", (canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}>
418
441
  <div
@@ -420,52 +443,86 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
420
443
  <Typography className={"mt-16 mb-8 mx-8"} variant={"h4"}>
421
444
  {collection.singularName ?? collection.name}
422
445
  </Typography>
423
- <EntityView
424
- className={"px-8 h-full overflow-auto"}
425
- entity={entity}
426
- path={path}
427
- collection={collection} />
446
+ {FormViewBuilder && readOnlyFormContext ? (
447
+ <ErrorBoundary>
448
+ <Suspense fallback={<CircularProgressCenter />}>
449
+ <FormViewBuilder
450
+ collection={collection}
451
+ parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
452
+ entity={usedEntity}
453
+ modifiedValues={usedEntity?.values}
454
+ formContext={readOnlyFormContext as FormContext<Record<string, unknown>>}
455
+ />
456
+ </Suspense>
457
+ </ErrorBoundary>
458
+ ) : (
459
+ <EntityView
460
+ className={"px-8 h-full overflow-auto"}
461
+ entity={entity}
462
+ path={path}
463
+ collection={collection} />
464
+ )}
428
465
  <div className="h-16" />
429
466
  </div>
430
467
  </div> : null;
431
468
 
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
- />;
469
+ const entityView = FormViewBuilder ? (
470
+ // formView.Builder replaces the default form
471
+ <div className={cls(
472
+ "relative flex-1 w-full h-full overflow-auto",
473
+ (!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : ""
474
+ )}>
475
+ <ErrorBoundary>
476
+ <Suspense fallback={<CircularProgressCenter />}>
477
+ {(formContext ?? readOnlyFormContext) && <FormViewBuilder
478
+ collection={collection}
479
+ parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
480
+ entity={usedEntity}
481
+ modifiedValues={(formContext ?? readOnlyFormContext)?.formex?.values ?? usedEntity?.values}
482
+ formContext={(formContext ?? readOnlyFormContext) as FormContext<Record<string, unknown>>}
483
+ />}
484
+ </Suspense>
485
+ </ErrorBoundary>
486
+ </div>
487
+ ) : (
488
+ <EntityForm<M>
489
+ collection={collection}
490
+ path={path}
491
+ entityId={entityId ?? usedEntity?.id}
492
+ onValuesModified={onValuesModified}
493
+ entity={entity}
494
+ initialDirtyValues={initialDirtyValues}
495
+ openEntityMode={layout}
496
+ forceActionsAtTheBottom={actionsAtTheBottom}
497
+ initialStatus={status}
498
+ className={cls((!mainViewVisible || !canEdit) && !selectedSecondaryForm ? "hidden" : "", formProps?.className)}
499
+ EntityFormActionsComponent={EntityEditViewFormActions}
500
+ disabled={!canEdit}
501
+ navigateBack={navigateBack}
502
+ {...formProps}
503
+ onEntityChange={(entity) => {
504
+ setUsedEntity(entity);
505
+ formProps?.onEntityChange?.(entity);
506
+ }}
507
+ onStatusChange={(status) => {
508
+ setStatus(status);
509
+ formProps?.onStatusChange?.(status);
510
+ }}
511
+ onFormContextReady={(formContext) => {
512
+ setFormContext(formContext);
513
+ formProps?.onFormContextReady?.(formContext);
514
+ }}
515
+ onSaved={(params) => {
516
+ const res = {
517
+ ...params,
518
+ selectedTab: MAIN_TAB_VALUE === selectedTab ? undefined : selectedTab
519
+ };
520
+ onSaved?.(res);
521
+ formProps?.onSaved?.(res);
522
+ }}
523
+ Builder={resolveComponentRef(selectedSecondaryForm?.Builder as ComponentRef<EntityCustomViewParams<M>> | undefined) as React.ComponentType<EntityCustomViewParams<M>> | undefined}
524
+ />
525
+ );
469
526
 
470
527
  const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
471
528
  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,34 @@ 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
+ if (hasCorrectedDefaultView.current) return;
107
+ if (selectedTab) return; // Already has a tab — no correction needed
108
+ if (!entityId || !collection?.defaultSelectedView) return;
109
+
110
+ const effectiveDefault = resolveDefaultSelectedView(
111
+ collection.defaultSelectedView,
112
+ { status: "existing", entityId }
113
+ );
114
+ if (effectiveDefault && effectiveDefault !== "edit") {
115
+ hasCorrectedDefaultView.current = true;
116
+ sideEntityController.replace({
117
+ path,
118
+ entityId,
119
+ selectedTab: effectiveDefault,
120
+ updateUrl: collection.openEntityMode !== "dialog",
121
+ collection
122
+ });
123
+ }
124
+ }, [selectedTab, entityId, collection, path, sideEntityController]);
125
+
99
126
  // Note: beforeunload is handled by useUnsavedChangesDialog in SideDialogView,
100
127
  // which listens to the same `blocked` state via SideDialogContext.
101
128
 
@@ -153,10 +180,13 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
153
180
  return;
154
181
  }
155
182
  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 });
183
+ sideEntityController.replace({
184
+ path,
185
+ entityId: tabEntityId,
186
+ selectedTab,
187
+ updateUrl: true,
188
+ collection: paramCollection ?? collection
189
+ });
160
190
  }
161
191
  }}
162
192
  />
@@ -198,18 +228,21 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
198
228
  </IconButton>}
199
229
  </div>}
200
230
  onTabChange={({
201
- entityId,
231
+ entityId: tabEntityId,
202
232
  selectedTab,
203
233
  collection: paramCollection
204
234
  }) => {
205
235
  if (collection?.openEntityMode === "dialog" || paramCollection?.openEntityMode === "dialog") {
206
236
  return;
207
237
  }
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 });
238
+ if (tabEntityId) {
239
+ sideEntityController.replace({
240
+ path,
241
+ entityId: tabEntityId,
242
+ selectedTab,
243
+ updateUrl: true,
244
+ collection: paramCollection ?? collection
245
+ });
213
246
  }
214
247
  }}
215
248
  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
@@ -1,8 +1,8 @@
1
1
  import { createSelectionStore } from "./SelectionStore";
2
2
  import type { Property } from "@rebasepro/types";
3
3
  import React, { useCallback, useEffect, useMemo, useRef } from "react";
4
- import { CollectionSize, Entity, EntityTableController, FilterValues, SelectedCellProps } from "@rebasepro/types";
5
- import { CellRendererParams, VirtualTable, VirtualTableColumn } from "@rebasepro/ui";
4
+ import { CollectionSize, Entity, EntityRelation, EntityTableController, FilterValues, SelectedCellProps } from "@rebasepro/types";
5
+ import { CellRendererParams, VirtualTable, VirtualTableColumn, VirtualTableFilterValues, OnRowClickParams, VirtualTableWhereFilterOp } from "@rebasepro/ui";
6
6
  import { enumToObjectEntries } from "@rebasepro/common";
7
7
  import { DEFAULT_PAGE_SIZE, EntityCollectionTableController, OnCellValueChange, OnColumnResizeParams } from "@rebasepro/core";
8
8
  import { FilterFormFieldProps } from "@rebasepro/ui";
@@ -20,7 +20,7 @@ export type SelectableTableProps<M extends Record<string, unknown>> = {
20
20
  /**
21
21
  * Callback when a cell value changes.
22
22
  */
23
- onValueChange?: OnCellValueChange<any, M>;
23
+ onValueChange?: OnCellValueChange<unknown, M>;
24
24
 
25
25
  columns: VirtualTableColumn[];
26
26
 
@@ -100,7 +100,7 @@ export type SelectableTableProps<M extends Record<string, unknown>> = {
100
100
  * bust the cell memo comparator, triggering re-render of cells
101
101
  * even when rowData hasn't changed.
102
102
  */
103
- extraData?: Record<string, any>;
103
+ extraData?: unknown;
104
104
  }
105
105
 
106
106
  /**
@@ -184,12 +184,10 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
184
184
  setItemCount?.(pageSize);
185
185
  }, [pageSize]);
186
186
 
187
- const onRowClick = useCallback(({ rowData }: {
188
- rowData: Entity<M>
189
- }) => {
187
+ const onRowClick = useCallback(({ rowData }: OnRowClickParams<Record<string, unknown>>) => {
190
188
  if (inlineEditing)
191
189
  return;
192
- return onEntityClick && onEntityClick(rowData);
190
+ return onEntityClick && onEntityClick(rowData as unknown as Entity<M>);
193
191
  }, [onEntityClick, inlineEditing]);
194
192
 
195
193
  const select = useCallback((cell?: SelectedCellProps<M>) => {
@@ -223,18 +221,22 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
223
221
  };
224
222
  }, [unselect]);
225
223
 
226
- const onFilterUpdate = useCallback((updatedFilterValues?: FilterValues<any>) => {
227
- setFilterValues?.({ ...updatedFilterValues,
228
- ...fixedFilter } as FilterValues<any>);
229
- }, [fixedFilter]);
224
+ const onFilterUpdate = useCallback((updatedFilterValues?: VirtualTableFilterValues<string>) => {
225
+ if (setFilterValues) {
226
+ setFilterValues({
227
+ ...updatedFilterValues,
228
+ ...fixedFilter
229
+ } as FilterValues<Extract<keyof M, string> | (string & {})>);
230
+ }
231
+ }, [fixedFilter, setFilterValues]);
230
232
 
231
233
  const contextValue = useMemo(() => ({
232
- setPopupCell: setPopupCell as ((cell?: SelectedCellProps<M>) => void),
233
234
  select,
234
235
  onValueChange,
235
236
  size: size ?? "m",
236
- selectionStore
237
- } as EntityCollectionTableController<any>), [setPopupCell, select, onValueChange, size, selectionStore]);
237
+ selectionStore,
238
+ setPopupCell
239
+ } as unknown as EntityCollectionTableController<Record<string, unknown>>), [setPopupCell, select, onValueChange, size, selectionStore]);
238
240
 
239
241
  return (
240
242
  <SelectableTableContext.Provider
@@ -243,11 +245,11 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
243
245
  ref={ref}>
244
246
 
245
247
  <VirtualTable
246
- data={data as any}
248
+ data={data as unknown as Record<string, unknown>[]}
247
249
  columns={columns}
248
250
  // @ts-ignore
249
251
  cellRenderer={cellRenderer}
250
- onRowClick={inlineEditing ? undefined : (onEntityClick ? (onRowClick as any) : undefined)}
252
+ onRowClick={inlineEditing ? undefined : (onEntityClick ? onRowClick : undefined)}
251
253
  onEndReached={loadNextPage}
252
254
  onResetPagination={resetPagination}
253
255
  error={dataLoadingError}
@@ -262,9 +264,9 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
262
264
  initialScroll={initialScroll}
263
265
  onScroll={onScroll}
264
266
  checkFilterCombination={checkFilterCombination}
265
- createFilterField={filterable ? (createFilterField as any) : undefined}
266
- rowClassName={useCallback((entity: any) => {
267
- return highlightedRow?.(entity) ? "bg-surface-accent-50 dark:!bg-surface-accent-900" : "";
267
+ createFilterField={filterable ? createFilterField : undefined}
268
+ rowClassName={useCallback((entity: Record<string, unknown>) => {
269
+ return highlightedRow?.(entity as unknown as Entity<M>) ? "bg-surface-accent-50 dark:!bg-surface-accent-900" : "";
268
270
  }, [highlightedRow])}
269
271
  className="grow"
270
272
  emptyComponent={emptyComponent}
@@ -287,7 +289,7 @@ function createFilterField({
287
289
  column,
288
290
  hidden,
289
291
  setHidden
290
- }: FilterFormFieldProps<any>): React.ReactNode {
292
+ }: FilterFormFieldProps<unknown>): React.ReactNode {
291
293
 
292
294
  if (!column.custom) {
293
295
  return null;
@@ -313,7 +315,7 @@ function createFilterField({
313
315
  hidden={hidden}
314
316
  setHidden={setHidden}/>;
315
317
  } else if (baseProperty.type === "relation" && baseProperty.relation) {
316
- return <RelationFilterField value={filterValue as any}
318
+ return <RelationFilterField value={filterValue as [VirtualTableWhereFilterOp, EntityRelation | EntityRelation[] | null]}
317
319
  setValue={setFilterValue}
318
320
  name={id as string}
319
321
  relation={baseProperty.relation}
@@ -3,4 +3,5 @@ import { EntityCollectionTableController } from "@rebasepro/core";
3
3
 
4
4
  export const SelectableTableContext = React.createContext<EntityCollectionTableController<Record<string, unknown>>>(null! as EntityCollectionTableController<Record<string, unknown>>);
5
5
 
6
- export const useSelectableTableController = () => useContext<EntityCollectionTableController<any>>(SelectableTableContext);
6
+ export const useSelectableTableController = <M extends Record<string, unknown> = Record<string, unknown>>() =>
7
+ useContext(SelectableTableContext) as unknown as EntityCollectionTableController<M>;
@@ -15,10 +15,10 @@ import { SelectedCellProps } from "@rebasepro/types";
15
15
  * derivation actually changed will re-render.
16
16
  */
17
17
  export function createSelectionStore() {
18
- let selectedCell: SelectedCellProps<any> | undefined = undefined;
18
+ let selectedCell: SelectedCellProps | undefined = undefined;
19
19
  const listeners = new Set<() => void>();
20
20
 
21
- function getSnapshot(): SelectedCellProps<any> | undefined {
21
+ function getSnapshot(): SelectedCellProps | undefined {
22
22
  return selectedCell;
23
23
  }
24
24
 
@@ -27,7 +27,7 @@ export function createSelectionStore() {
27
27
  return () => listeners.delete(listener);
28
28
  }
29
29
 
30
- function select(cell: SelectedCellProps<any> | undefined) {
30
+ function select(cell: SelectedCellProps | undefined) {
31
31
  selectedCell = cell;
32
32
  listeners.forEach(l => l());
33
33
  }
@@ -43,7 +43,7 @@ export type SelectionStore = ReturnType<typeof createSelectionStore>;
43
43
  * `selected` boolean actually changes (not on every store update).
44
44
  */
45
45
  export function useCellSelected(
46
- store: { getSnapshot: () => SelectedCellProps<any> | undefined; subscribe: (listener: () => void) => () => void },
46
+ store: { getSnapshot: () => SelectedCellProps | undefined; subscribe: (listener: () => void) => () => void },
47
47
  propertyKey: string,
48
48
  entityPath: string,
49
49
  entityId: string | number
@@ -53,3 +53,5 @@ export * from "./RebaseAuthGate";
53
53
  export * from "./RebaseNavigation";
54
54
  export * from "./RebaseLayout";
55
55
  export * from "./RebaseRouteDefs";
56
+
57
+ export * from "./CollectionPanel";