@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.
- package/README.md +135 -0
- package/dist/{CollectionEditorDialog-D0VqpLPO.js → CollectionEditorDialog-C8E440oK.js} +22 -5
- package/dist/CollectionEditorDialog-C8E440oK.js.map +1 -0
- package/dist/{CollectionsStudioView-Bc3Rxxc2.js → CollectionsStudioView-Brr_b06v.js} +4 -4
- package/dist/{CollectionsStudioView-Bc3Rxxc2.js.map → CollectionsStudioView-Brr_b06v.js.map} +1 -1
- package/dist/{ExportCollectionAction-Ckc-09BQ.js → ExportCollectionAction-D3TTYXrq.js} +35 -27
- package/dist/ExportCollectionAction-D3TTYXrq.js.map +1 -0
- package/dist/{ImportCollectionAction-BqjIrC3Z.js → ImportCollectionAction-Dn7lya6E.js} +2 -2
- package/dist/{ImportCollectionAction-BqjIrC3Z.js.map → ImportCollectionAction-Dn7lya6E.js.map} +1 -1
- package/dist/{PropertyEditView-CvRSV-A2.js → PropertyEditView-ChBleIbN.js} +4 -4
- package/dist/PropertyEditView-ChBleIbN.js.map +1 -0
- package/dist/collection_editor/ui/collection_editor/PropertyEditView.d.ts +1 -0
- package/dist/collection_editor/ui/collection_editor/templates/blog_template.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/templates/pages_template.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/templates/products_template.d.ts +1 -1
- package/dist/collection_editor/ui/collection_editor/templates/users_template.d.ts +1 -1
- package/dist/collection_editor_ui.js +3 -3
- package/dist/components/CollectionPanel.d.ts +83 -0
- package/dist/components/EntityDetailView.d.ts +0 -3
- package/dist/components/EntityEditView.d.ts +2 -3
- package/dist/components/RebaseRouteDefs.d.ts +1 -1
- package/dist/components/SelectableTable/SelectableTable.d.ts +2 -2
- package/dist/components/SelectableTable/SelectableTableContext.d.ts +1 -1
- package/dist/components/SelectableTable/SelectionStore.d.ts +3 -3
- package/dist/components/admin/index.d.ts +1 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/editor.js +15 -5
- package/dist/editor.js.map +1 -1
- package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
- package/dist/hooks/navigation/useResolvedViews.d.ts +2 -5
- package/dist/{index-BCcLwgfe.js → index-D3S9x_RP.js} +2 -2
- package/dist/{index-BCcLwgfe.js.map → index-D3S9x_RP.js.map} +1 -1
- package/dist/{index-DY2k5TtG.js → index-D9RNzm01.js} +3 -3
- package/dist/index-D9RNzm01.js.map +1 -0
- package/dist/{index-UQOMHwt1.js → index-DrrT332R.js} +3 -3
- package/dist/index-DrrT332R.js.map +1 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +228 -295
- package/dist/index.js.map +1 -1
- package/dist/util/entity_view_constants.d.ts +6 -0
- package/dist/util/resolutions.d.ts +1 -2
- package/dist/{util-ZM9gQuCv.js → util-COYYD8zr.js} +473 -880
- package/dist/util-COYYD8zr.js.map +1 -0
- package/package.json +9 -10
- package/src/collection_editor/pgColumnToProperty.ts +19 -2
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +2 -2
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +2 -1
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +2 -2
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +2 -1
- package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/products_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/users_template.ts +1 -1
- package/src/collection_editor/validateCollectionJson.ts +12 -12
- package/src/components/CollectionPanel.tsx +155 -0
- package/src/components/DefaultDrawer.tsx +2 -2
- package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +5 -2
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +64 -34
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +8 -1
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +4 -1
- package/src/components/EntityCollectionView/hooks/useCollectionInlineEditor.ts +1 -0
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +68 -18
- package/src/components/EntityDetailView.tsx +32 -36
- package/src/components/EntityEditView.tsx +101 -50
- package/src/components/EntityEditViewFormActions.tsx +4 -4
- package/src/components/EntitySidePanel.tsx +50 -14
- package/src/components/PropertyCollectionView.tsx +1 -0
- package/src/components/RebaseRouteDefs.tsx +4 -6
- package/src/components/SelectableTable/SelectableTable.tsx +24 -22
- package/src/components/SelectableTable/SelectableTableContext.tsx +2 -1
- package/src/components/SelectableTable/SelectionStore.ts +4 -4
- package/src/components/admin/index.ts +1 -3
- package/src/components/index.ts +3 -3
- package/src/data_export/export/BasicExportAction.tsx +11 -9
- package/src/data_export/export/ExportCollectionAction.tsx +15 -13
- package/src/editor/editor.tsx +20 -4
- package/src/form/EntityForm.tsx +3 -3
- package/src/form/PropertyFieldBinding.tsx +1 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +1 -1
- package/src/form/field_bindings/MapFieldBinding.tsx +3 -45
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -1
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +2 -3
- package/src/hooks/navigation/useResolvedCollections.ts +2 -10
- package/src/hooks/navigation/useResolvedViews.tsx +6 -48
- package/src/hooks/useBuildSideEntityController.tsx +20 -3
- package/src/index.ts +6 -5
- package/src/util/entity_view_constants.ts +6 -0
- package/src/util/previews.ts +9 -1
- package/src/util/resolutions.ts +2 -6
- package/dist/CollectionEditorDialog-D0VqpLPO.js.map +0 -1
- package/dist/ExportCollectionAction-Ckc-09BQ.js.map +0 -1
- package/dist/PropertyEditView-CvRSV-A2.js.map +0 -1
- package/dist/components/admin/RoleChip.d.ts +0 -4
- package/dist/components/admin/RolesFilterSelect.d.ts +0 -2
- package/dist/components/admin/RolesView.d.ts +0 -4
- package/dist/components/admin/UserRolesSelectField.d.ts +0 -2
- package/dist/components/admin/UsersView.d.ts +0 -4
- package/dist/index-DY2k5TtG.js.map +0 -1
- package/dist/index-UQOMHwt1.js.map +0 -1
- package/dist/util-ZM9gQuCv.js.map +0 -1
- package/src/components/admin/RoleChip.tsx +0 -23
- package/src/components/admin/RolesFilterSelect.tsx +0 -45
- package/src/components/admin/RolesView.tsx +0 -470
- package/src/components/admin/UserRolesSelectField.tsx +0 -50
- package/src/components/admin/UsersView.tsx +0 -693
|
@@ -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
|
-
//
|
|
433
|
+
// Resolve target collection from either `prop.relation.target()` or `prop.target()` (inline API)
|
|
434
|
+
let targetCollection: EntityCollection | undefined;
|
|
440
435
|
try {
|
|
441
|
-
const
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
458
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
227
|
-
const
|
|
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
|
-
//
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
54
|
-
export
|
|
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
|
-
|
|
229
|
-
|
|
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
|
|
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
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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 =
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
formProps
|
|
466
|
-
|
|
467
|
-
|
|
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 {
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
86
|
+
|
|
89
87
|
|
|
90
88
|
{/* Hidden debug route */}
|
|
91
89
|
<Route path={"/debug/ui"} element={<UIReferenceView/>}/>
|