@rebasepro/admin 0.0.1-canary.f81da60 → 0.1.2
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/dist/{CollectionEditorDialog-D509-IMx.js → CollectionEditorDialog-ywdxhs1L.js} +18 -18
- package/dist/CollectionEditorDialog-ywdxhs1L.js.map +1 -0
- package/dist/{CollectionsStudioView-B549BDpU.js → CollectionsStudioView-BDzMFzqH.js} +4 -4
- package/dist/{CollectionsStudioView-B549BDpU.js.map → CollectionsStudioView-BDzMFzqH.js.map} +1 -1
- package/dist/{ContentHomePage--Bl1FXk7.js → ContentHomePage-0tHuEIm_.js} +26 -26
- package/dist/ContentHomePage-0tHuEIm_.js.map +1 -0
- package/dist/{ExportCollectionAction-CttNAdM1.js → ExportCollectionAction-BIrq92To.js} +2 -2
- package/dist/{ExportCollectionAction-CttNAdM1.js.map → ExportCollectionAction-BIrq92To.js.map} +1 -1
- package/dist/{ImportCollectionAction-BB33kxAN.js → ImportCollectionAction-h8yg_To8.js} +2 -2
- package/dist/{ImportCollectionAction-BB33kxAN.js.map → ImportCollectionAction-h8yg_To8.js.map} +1 -1
- package/dist/{PropertyEditView-UtDO8g0A.js → PropertyEditView-BuZrNnBN.js} +79 -101
- package/dist/PropertyEditView-BuZrNnBN.js.map +1 -0
- package/dist/{RolesView-B0E7L0hE.js → RolesView-CMPsaIXo.js} +2 -2
- package/dist/{RolesView-B0E7L0hE.js.map → RolesView-CMPsaIXo.js.map} +1 -1
- package/dist/{UsersView-BM2_7VPV.js → UsersView-BkeblMVT.js} +6 -28
- package/dist/UsersView-BkeblMVT.js.map +1 -0
- package/dist/collection_editor/ConfigControllerProvider.d.ts +0 -4
- package/dist/collection_editor/ui/collection_editor/CollectionDetailsForm.d.ts +1 -3
- package/dist/collection_editor/ui/collection_editor/CollectionEditorDialog.d.ts +0 -2
- package/dist/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +1 -2
- package/dist/collection_editor_ui.js +3 -3
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +2 -1
- package/dist/components/EntityEditView.d.ts +6 -0
- package/dist/components/RebaseCMS.d.ts +1 -1
- package/dist/{index-C9YDsMC9.js → index-BuZaHcyc.js} +3 -3
- package/dist/index-BuZaHcyc.js.map +1 -0
- package/dist/{index-CNDetux9.js → index-CS6uJ7oW.js} +2 -2
- package/dist/{index-CNDetux9.js.map → index-CS6uJ7oW.js.map} +1 -1
- package/dist/{index-DO7lMeNB.js → index-eRJbMvHi.js} +3 -3
- package/dist/index-eRJbMvHi.js.map +1 -0
- package/dist/index.js +18 -14
- package/dist/index.js.map +1 -1
- package/dist/util/navigation_utils.d.ts +10 -1
- package/dist/{util-DK1O3uM0.js → util-zfU1zOCX.js} +713 -603
- package/dist/util-zfU1zOCX.js.map +1 -0
- package/package.json +8 -8
- package/src/collection_editor/ConfigControllerProvider.tsx +1 -10
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +3 -47
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +2 -10
- package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +1 -3
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +3 -3
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +0 -1
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +6 -6
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/ReferencePropertyField.tsx +15 -49
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +2 -3
- package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +1 -1
- package/src/collection_editor/ui/collection_editor/templates/products_template.ts +2 -2
- package/src/components/DefaultAppBar.tsx +2 -2
- package/src/components/DefaultDrawer.tsx +25 -17
- package/src/components/DrawerNavigationGroup.tsx +4 -4
- package/src/components/DrawerNavigationItem.tsx +6 -6
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +5 -3
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +8 -2
- package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +2 -2
- package/src/components/EntityCollectionTable/table_bindings.tsx +37 -27
- package/src/components/EntityCollectionView/EntityCard.tsx +2 -2
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +4 -3
- package/src/components/EntityCollectionView/EntityCollectionListView.tsx +7 -6
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +50 -7
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +17 -8
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -4
- package/src/components/EntityCollectionView/useEntityPreviewSlots.ts +33 -5
- package/src/components/EntityEditView.tsx +80 -81
- package/src/components/EntitySidePanel.tsx +11 -7
- package/src/components/HomePage/ContentHomePage.tsx +24 -15
- package/src/components/HomePage/NavigationCard.tsx +4 -4
- package/src/components/HomePage/NavigationGroup.tsx +2 -2
- package/src/components/RebaseAuthGate.tsx +2 -0
- package/src/components/RebaseCMS.tsx +4 -3
- package/src/components/RebaseNavigation.tsx +7 -4
- package/src/components/RelationSelector.tsx +30 -2
- package/src/components/SelectableTable/SelectableTable.tsx +2 -2
- package/src/components/UserSelector.tsx +1 -1
- package/src/components/admin/UsersView.tsx +2 -17
- package/src/components/app/Scaffold.tsx +3 -3
- package/src/components/field_configs.tsx +3 -3
- package/src/form/PropertyFieldBinding.tsx +10 -6
- package/src/hooks/navigation/useResolvedViews.tsx +1 -3
- package/src/hooks/navigation/useTopLevelNavigation.ts +1 -1
- package/src/hooks/navigation/utils.ts +1 -1
- package/src/preview/PropertyPreview.tsx +17 -13
- package/src/routes/RebaseRoute.tsx +27 -2
- package/src/util/navigation_utils.ts +16 -2
- package/src/util/previews.ts +14 -5
- package/dist/CollectionEditorDialog-D509-IMx.js.map +0 -1
- package/dist/ContentHomePage--Bl1FXk7.js.map +0 -1
- package/dist/PropertyEditView-UtDO8g0A.js.map +0 -1
- package/dist/UsersView-BM2_7VPV.js.map +0 -1
- package/dist/index-C9YDsMC9.js.map +0 -1
- package/dist/index-DO7lMeNB.js.map +0 -1
- package/dist/util-DK1O3uM0.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
|
|
1
|
+
import type { ComponentRef, EntityCollection, EntityCustomViewParams } from "@rebasepro/types";
|
|
2
2
|
import type { FormContext } from "../types/fields";
|
|
3
3
|
import type { PluginFormActionProps } from "@rebasepro/types";
|
|
4
|
-
import React, { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { Entity, EntityStatus } from "@rebasepro/types";
|
|
6
|
-
import { PluginProviderStack } from "@rebasepro/core";
|
|
6
|
+
import { PluginProviderStack, resolveComponentRef } from "@rebasepro/core";
|
|
7
7
|
|
|
8
8
|
import { EntityCollectionView, EntityView } from "../components";
|
|
9
9
|
import { CircularProgressCenter, iconSize } from "@rebasepro/ui";
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
resolveDefaultSelectedView
|
|
17
17
|
} from "@rebasepro/common";
|
|
18
18
|
import { resolvedSelectedEntityView } from "../util/resolutions";
|
|
19
|
-
import { getEntityTitlePropertyKey } from "../util/previews";
|
|
20
19
|
import { CenteredView, CircularProgress, cls, defaultBorderMixin, IconButton, Tab, Tabs, Tooltip, Typography, Skeleton } from "@rebasepro/ui";
|
|
21
20
|
import {
|
|
22
21
|
useCustomizationController,
|
|
@@ -73,6 +72,12 @@ export interface EntityEditViewProps<M extends Record<string, unknown> = Record<
|
|
|
73
72
|
layout?: "side_panel" | "full_screen" | "split";
|
|
74
73
|
barActions?: (params: BarActionsParams) => any;
|
|
75
74
|
formProps?: Partial<EntityFormProps<M>>,
|
|
75
|
+
/**
|
|
76
|
+
* Pre-populate the form with these values when creating a new entity.
|
|
77
|
+
* Only applied when the form is in "new" mode (no entityId).
|
|
78
|
+
* Sourced from EntitySidePanelProps (side panel) or location.state (full screen).
|
|
79
|
+
*/
|
|
80
|
+
defaultValues?: Partial<M>;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
@@ -99,7 +104,7 @@ export function EntityEditView<M extends Record<string, unknown>>({
|
|
|
99
104
|
|
|
100
105
|
const initialDirtyValues = entityId
|
|
101
106
|
? getEntityFromMemoryCache(props.path + "/" + entityId)
|
|
102
|
-
: getEntityFromMemoryCache(props.path + "#new");
|
|
107
|
+
: (props.defaultValues ?? getEntityFromMemoryCache(props.path + "#new"));
|
|
103
108
|
|
|
104
109
|
const { canEdit: canEditHook } = usePermissions();
|
|
105
110
|
|
|
@@ -183,7 +188,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
183
188
|
const customizationController = useCustomizationController();
|
|
184
189
|
const plugins = customizationController.plugins;
|
|
185
190
|
|
|
186
|
-
const formActionTopProps: PluginFormActionProps = {
|
|
191
|
+
const formActionTopProps: PluginFormActionProps = useMemo(() => ({
|
|
187
192
|
entityId,
|
|
188
193
|
parentCollectionSlugs, parentEntityIds,
|
|
189
194
|
path: path,
|
|
@@ -193,7 +198,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
193
198
|
formContext: formContext as FormContext<Record<string, unknown>> | undefined,
|
|
194
199
|
openEntityMode: layout,
|
|
195
200
|
disabled: false
|
|
196
|
-
};
|
|
201
|
+
}), [entityId, parentCollectionSlugs, parentEntityIds, path, status, collection, context, formContext, layout]);
|
|
197
202
|
const pluginActionsTop = useSlot("form.actions.top", formActionTopProps);
|
|
198
203
|
|
|
199
204
|
const defaultSelectedView = useMemo(() => resolveDefaultSelectedView(
|
|
@@ -230,15 +235,51 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
230
235
|
|
|
231
236
|
const mainViewVisible = selectedTab === MAIN_TAB_VALUE || Boolean(selectedSecondaryForm);
|
|
232
237
|
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
// Track which custom view tabs have been visited so we keep them mounted
|
|
239
|
+
// (preserving their state) but don't eagerly mount tabs never visited.
|
|
240
|
+
const mountedTabsRef = useRef<Set<string>>(new Set());
|
|
241
|
+
if (selectedTab) {
|
|
242
|
+
mountedTabsRef.current.add(selectedTab);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Memoize the read-only fallback form context to avoid recreating it every render
|
|
246
|
+
const readOnlyFormContext = useMemo<FormContext<M> | undefined>(() => {
|
|
247
|
+
if (formContext) return undefined; // not needed when real formContext exists
|
|
248
|
+
if (!entityId) return undefined;
|
|
249
|
+
const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
|
|
250
|
+
return {
|
|
251
|
+
entityId,
|
|
252
|
+
disabled: false,
|
|
253
|
+
openEntityMode: layout,
|
|
254
|
+
status: status,
|
|
255
|
+
values: usedEntity?.values ?? ({} as M),
|
|
256
|
+
setFieldValue: (key: string, value: any) => {
|
|
257
|
+
throw new Error("You can't update values in read only mode");
|
|
258
|
+
},
|
|
259
|
+
save: () => {
|
|
260
|
+
throw new Error("You can't save in read only mode");
|
|
261
|
+
},
|
|
262
|
+
collection,
|
|
263
|
+
path: path,
|
|
264
|
+
entity: usedEntity,
|
|
265
|
+
savingError: undefined,
|
|
266
|
+
formex: formexStub
|
|
267
|
+
};
|
|
268
|
+
}, [formContext, entityId, layout, status, usedEntity, collection, path]);
|
|
269
|
+
|
|
270
|
+
const nonActionCustomViews = useMemo(() =>
|
|
271
|
+
resolvedEntityViews.filter(e => !e.includeActions),
|
|
272
|
+
[resolvedEntityViews]
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const customViewsView: any[] | undefined = customViews && nonActionCustomViews
|
|
235
276
|
.map((customView) => {
|
|
236
277
|
|
|
237
278
|
if (!customView)
|
|
238
279
|
return null;
|
|
239
|
-
const Builder = customView.Builder;
|
|
280
|
+
const Builder = resolveComponentRef<EntityCustomViewParams>(customView.Builder);
|
|
240
281
|
if (!Builder) {
|
|
241
|
-
console.error("INTERNAL: customView.Builder is not defined");
|
|
282
|
+
console.error("INTERNAL: customView.Builder is not defined or could not be resolved");
|
|
242
283
|
return null;
|
|
243
284
|
}
|
|
244
285
|
|
|
@@ -246,48 +287,41 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
246
287
|
return null;
|
|
247
288
|
}
|
|
248
289
|
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
throw new Error("You can't update values in read only mode");
|
|
258
|
-
},
|
|
259
|
-
save: () => {
|
|
260
|
-
throw new Error("You can't save in read only mode");
|
|
261
|
-
},
|
|
262
|
-
collection,
|
|
263
|
-
path: path,
|
|
264
|
-
entity: usedEntity,
|
|
265
|
-
savingError: undefined,
|
|
266
|
-
formex: formexStub
|
|
267
|
-
};
|
|
290
|
+
// Only mount tabs that have been visited at least once
|
|
291
|
+
const isActive = selectedTab === customView.key;
|
|
292
|
+
const hasBeenMounted = mountedTabsRef.current.has(customView.key);
|
|
293
|
+
if (!isActive && !hasBeenMounted) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const usedFormContext: FormContext<M> = formContext ?? readOnlyFormContext!;
|
|
268
298
|
|
|
269
299
|
return <div
|
|
270
300
|
className={cls(defaultBorderMixin,
|
|
271
301
|
"relative flex-1 w-full h-full overflow-auto",
|
|
272
|
-
{ "hidden":
|
|
302
|
+
{ "hidden": !isActive }
|
|
273
303
|
)}
|
|
274
304
|
key={`custom_view_${customView.key}`}
|
|
275
305
|
role="tabpanel">
|
|
276
306
|
<ErrorBoundary>
|
|
277
|
-
{
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
307
|
+
<Suspense fallback={<CircularProgressCenter />}>
|
|
308
|
+
{usedFormContext && <Builder
|
|
309
|
+
collection={collection}
|
|
310
|
+
parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
|
|
311
|
+
entity={usedEntity}
|
|
312
|
+
modifiedValues={usedFormContext?.formex?.values ?? usedEntity?.values}
|
|
313
|
+
formContext={usedFormContext as unknown as FormContext<Record<string, unknown>>}
|
|
314
|
+
/>}
|
|
315
|
+
</Suspense>
|
|
284
316
|
</ErrorBoundary>
|
|
285
317
|
</div>;
|
|
286
318
|
}).filter(Boolean);
|
|
287
319
|
|
|
288
320
|
const globalLoading = (dataLoading && !usedEntity) || (canEdit === undefined && (status === "existing" || status === "copy"));
|
|
289
321
|
|
|
290
|
-
|
|
322
|
+
// Only mount JSON view when its tab is selected (or was previously selected)
|
|
323
|
+
const jsonTabMounted = mountedTabsRef.current.has(JSON_TAB_VALUE);
|
|
324
|
+
const jsonView = (selectedTab === JSON_TAB_VALUE || jsonTabMounted) ? <div
|
|
291
325
|
className={cls("relative flex-1 h-full overflow-auto w-full",
|
|
292
326
|
{ "hidden": selectedTab !== JSON_TAB_VALUE })}
|
|
293
327
|
key={"json_view"}
|
|
@@ -296,11 +330,11 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
296
330
|
<EntityJsonPreview
|
|
297
331
|
values={formContext?.values ?? entity?.values ?? {}} />
|
|
298
332
|
</ErrorBoundary>
|
|
299
|
-
</div
|
|
333
|
+
</div> : null;
|
|
300
334
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
335
|
+
// Only mount history view when its tab is actually selected
|
|
336
|
+
const historyView = includeHistoryView && selectedTab === HISTORY_TAB_VALUE ? <div
|
|
337
|
+
className={"relative flex-1 h-full overflow-auto w-full"}
|
|
304
338
|
key={"history_view"}
|
|
305
339
|
role="tabpanel">
|
|
306
340
|
<ErrorBoundary>
|
|
@@ -349,7 +383,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
349
383
|
);
|
|
350
384
|
}).filter(Boolean);
|
|
351
385
|
|
|
352
|
-
const onSideTabClick = (value: string) => {
|
|
386
|
+
const onSideTabClick = useCallback((value: string) => {
|
|
353
387
|
setSelectedTab(value);
|
|
354
388
|
if (status === "existing") {
|
|
355
389
|
onTabChange?.({
|
|
@@ -359,7 +393,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
359
393
|
collection
|
|
360
394
|
});
|
|
361
395
|
}
|
|
362
|
-
};
|
|
396
|
+
}, [status, onTabChange, path, entityId, collection]);
|
|
363
397
|
|
|
364
398
|
const entityReadOnlyView = !canEdit && entity ? <div
|
|
365
399
|
className={cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", (canEdit || !mainViewVisible || selectedSecondaryForm) ? "hidden" : "")}>
|
|
@@ -411,7 +445,7 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
411
445
|
onSaved?.(res);
|
|
412
446
|
formProps?.onSaved?.(res);
|
|
413
447
|
}}
|
|
414
|
-
Builder={selectedSecondaryForm?.Builder as React.ComponentType<EntityCustomViewParams<M>> | undefined}
|
|
448
|
+
Builder={resolveComponentRef(selectedSecondaryForm?.Builder as ComponentRef<EntityCustomViewParams<M>> | undefined) as React.ComponentType<EntityCustomViewParams<M>> | undefined}
|
|
415
449
|
/>;
|
|
416
450
|
|
|
417
451
|
const subcollectionTabs = subcollections && subcollections.map((subcollection) =>
|
|
@@ -458,33 +492,6 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
458
492
|
</Tooltip>
|
|
459
493
|
) : null;
|
|
460
494
|
|
|
461
|
-
// Compute contextual title for subcollection tabs, e.g. "Orders of James"
|
|
462
|
-
const subcollectionContextTitle = useMemo(() => {
|
|
463
|
-
if (selectedTab === MAIN_TAB_VALUE || selectedTab === JSON_TAB_VALUE || selectedTab === HISTORY_TAB_VALUE) {
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
// Check if the selected tab is a subcollection
|
|
467
|
-
const matchedSubcollection = subcollections.find(sc => sc.slug === selectedTab);
|
|
468
|
-
if (!matchedSubcollection) {
|
|
469
|
-
return null;
|
|
470
|
-
}
|
|
471
|
-
// Check if the selected tab is a custom view (not a subcollection)
|
|
472
|
-
const isCustomView = resolvedEntityViews.some(v => v.key === selectedTab);
|
|
473
|
-
if (isCustomView) {
|
|
474
|
-
return null;
|
|
475
|
-
}
|
|
476
|
-
// Resolve the parent entity's title
|
|
477
|
-
const titleKey = getEntityTitlePropertyKey(collection, customizationController.propertyConfigs);
|
|
478
|
-
const entityValues = usedEntity?.values;
|
|
479
|
-
if (!titleKey || !entityValues) {
|
|
480
|
-
return matchedSubcollection.name;
|
|
481
|
-
}
|
|
482
|
-
const titleValue = entityValues[titleKey as keyof M];
|
|
483
|
-
if (!titleValue || typeof titleValue !== "string") {
|
|
484
|
-
return matchedSubcollection.name;
|
|
485
|
-
}
|
|
486
|
-
return `${matchedSubcollection.name} of ${titleValue}`;
|
|
487
|
-
}, [selectedTab, subcollections, resolvedEntityViews, collection, customizationController.propertyConfigs, usedEntity?.values]);
|
|
488
495
|
|
|
489
496
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-800">
|
|
490
497
|
|
|
@@ -500,14 +507,6 @@ export function EntityEditViewInner<M extends Record<string, unknown>>({
|
|
|
500
507
|
status
|
|
501
508
|
})}
|
|
502
509
|
|
|
503
|
-
{subcollectionContextTitle && (
|
|
504
|
-
<Typography
|
|
505
|
-
variant="label"
|
|
506
|
-
className="truncate min-w-0 shrink ml-2 text-surface-600 dark:text-surface-400"
|
|
507
|
-
>
|
|
508
|
-
{subcollectionContextTitle}
|
|
509
|
-
</Typography>
|
|
510
|
-
)}
|
|
511
510
|
|
|
512
511
|
{pluginActionsTop}
|
|
513
512
|
|
|
@@ -9,6 +9,7 @@ import { EntityEditView } from "./EntityEditView";
|
|
|
9
9
|
import { useSideDialogContext } from "./SideDialogs";
|
|
10
10
|
import { IconButton } from "@rebasepro/ui";
|
|
11
11
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
12
|
+
import { removeInitialAndTrailingSlashes } from "@rebasepro/common";
|
|
12
13
|
import { saveEntityToMemoryCache } from "@rebasepro/core";
|
|
13
14
|
import { useCollectionRegistryController, useSideEntityController } from "../index";
|
|
14
15
|
import { useUrlController } from "../index";
|
|
@@ -147,13 +148,16 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
147
148
|
selectedTab,
|
|
148
149
|
collection
|
|
149
150
|
}) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
// Only update the URL to reflect the new tab — don't call
|
|
152
|
+
// sideEntityController.replace() which would recreate the
|
|
153
|
+
// entire EntitySidePanel component, causing a full
|
|
154
|
+
// unmount/remount and expensive re-render of the form.
|
|
155
|
+
if (entityId) {
|
|
156
|
+
const collectionPath = removeInitialAndTrailingSlashes(path);
|
|
157
|
+
const tabSuffix = selectedTab ? "/" + selectedTab : "";
|
|
158
|
+
const fullUrl = urlController.buildUrlCollectionPath(`${collectionPath}/${entityId}${tabSuffix}#side`);
|
|
159
|
+
navigate(fullUrl, { replace: true, state: location.state });
|
|
160
|
+
}
|
|
157
161
|
}}
|
|
158
162
|
formProps={formProps}
|
|
159
163
|
/>
|
|
@@ -19,9 +19,10 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
|
|
|
19
19
|
import { restrictToVerticalAxis, restrictToWindowEdges } from "@dnd-kit/modifiers";
|
|
20
20
|
import { RenameGroupDialog } from "./RenameGroupDialog";
|
|
21
21
|
import { toArray } from "@rebasepro/utils";
|
|
22
|
-
import { useCollapsedGroups, useCustomizationController, useTranslation, useSlot, useAdminModeController } from "@rebasepro/core";
|
|
22
|
+
import { useCollapsedGroups, buildCollapsedDefaults, useCustomizationController, useTranslation, useSlot, useAdminModeController, useRebaseRegistry } from "@rebasepro/core";
|
|
23
23
|
import { useRestoreScroll } from "@rebasepro/core";
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
import { BootstrapAdminBanner } from "@rebasepro/core";
|
|
25
26
|
import { useBreadcrumbsController, useCMSContext } from "../../index";
|
|
26
27
|
|
|
27
28
|
export const DEFAULT_GROUP_NAME = "Views";
|
|
@@ -45,7 +46,8 @@ export function ContentHomePage({
|
|
|
45
46
|
const { navigationStateController } = context;
|
|
46
47
|
const customizationController = useCustomizationController();
|
|
47
48
|
const adminModeController = useAdminModeController();
|
|
48
|
-
const
|
|
49
|
+
const registry = useRebaseRegistry();
|
|
50
|
+
|
|
49
51
|
const { resolvedSlots } = customizationController;
|
|
50
52
|
const breadcrumbs = useBreadcrumbsController();
|
|
51
53
|
const { t } = useTranslation();
|
|
@@ -61,19 +63,19 @@ export function ContentHomePage({
|
|
|
61
63
|
onNavigationEntriesUpdate = () => {}
|
|
62
64
|
} = navigationStateController.topLevelNavigation || {};
|
|
63
65
|
|
|
66
|
+
// Studio mode shows view-type entries (devViews) + admin entries (Users/Roles).
|
|
67
|
+
// Content mode shows collections, admin entries (Users/Roles), and custom entries — but not studio views.
|
|
64
68
|
const rawNavigationEntries = useMemo(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}, [unFilteredNavigationEntries,
|
|
69
|
+
if (adminModeController.mode === "studio") {
|
|
70
|
+
return unFilteredNavigationEntries.filter(e => e.type === "view" || e.type === "admin");
|
|
71
|
+
}
|
|
72
|
+
return unFilteredNavigationEntries.filter(e => e.type !== "view");
|
|
73
|
+
}, [unFilteredNavigationEntries, adminModeController.mode]);
|
|
70
74
|
|
|
71
75
|
const groupOrderFromNavController = useMemo(() => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
}, [unFilteredGroupOrder, isStudioMode]);
|
|
76
|
+
const entryGroups = new Set(rawNavigationEntries.map(e => e.group).filter(Boolean));
|
|
77
|
+
return unFilteredGroupOrder.filter(g => entryGroups.has(g));
|
|
78
|
+
}, [unFilteredGroupOrder, rawNavigationEntries]);
|
|
77
79
|
|
|
78
80
|
const fuse = useRef<Fuse<NavigationEntry> | null>(null);
|
|
79
81
|
const [filteredUrls, setFilteredUrls] = useState<string[] | null>(null);
|
|
@@ -244,7 +246,11 @@ export function ContentHomePage({
|
|
|
244
246
|
...(adminGroupData ? [adminGroupData.name] : [])
|
|
245
247
|
], [items, adminGroupData]);
|
|
246
248
|
|
|
247
|
-
const
|
|
249
|
+
const collapsedDefaults = useMemo(
|
|
250
|
+
() => buildCollapsedDefaults(registry.cmsConfig?.navigationGroupMappings, "home"),
|
|
251
|
+
[registry.cmsConfig?.navigationGroupMappings]
|
|
252
|
+
);
|
|
253
|
+
const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupNames, "home", collapsedDefaults);
|
|
248
254
|
|
|
249
255
|
const {
|
|
250
256
|
sensors,
|
|
@@ -312,8 +318,11 @@ export function ContentHomePage({
|
|
|
312
318
|
Render
|
|
313
319
|
─────────────────────────────────────────────────────────────── */
|
|
314
320
|
return (
|
|
315
|
-
<div ref={containerRef} className="py-2 overflow-auto h-full w-full bg-surface-
|
|
321
|
+
<div ref={containerRef} className="py-2 overflow-auto h-full w-full bg-surface-50 dark:bg-surface-800">
|
|
316
322
|
<Container maxWidth="6xl">
|
|
323
|
+
<div className="mb-4">
|
|
324
|
+
<BootstrapAdminBanner />
|
|
325
|
+
</div>
|
|
317
326
|
{/* search & actions */}
|
|
318
327
|
<div
|
|
319
328
|
className="w-full sticky py-4 transition-all duration-400 ease-in-out top-0 z-10 flex flex-row gap-4"
|
|
@@ -26,10 +26,10 @@ export const NavigationCard = React.memo(function NavigationCard({
|
|
|
26
26
|
return (
|
|
27
27
|
<Card
|
|
28
28
|
className={cls(
|
|
29
|
-
"group h-full p-
|
|
30
|
-
"border-surface-200
|
|
31
|
-
"hover
|
|
32
|
-
"hover:border-
|
|
29
|
+
"group h-full p-4 cursor-pointer transition-all duration-150 ease-in-out",
|
|
30
|
+
"border-surface-200 dark:border-surface-700/40",
|
|
31
|
+
"hover:shadow-md hover:shadow-black/[0.04]",
|
|
32
|
+
"hover:border-surface-300 dark:hover:border-primary/20",
|
|
33
33
|
shrink && "w-full max-w-full min-h-0 scale-75"
|
|
34
34
|
)}
|
|
35
35
|
onClick={() => {
|
|
@@ -40,8 +40,8 @@ export function NavigationGroup({
|
|
|
40
40
|
component={"h2"}
|
|
41
41
|
color="secondary"
|
|
42
42
|
className={cls(
|
|
43
|
-
"
|
|
44
|
-
"font-
|
|
43
|
+
"px-4 py-1 rounded",
|
|
44
|
+
"font-semibold text-[11px] uppercase tracking-wider text-surface-400 dark:text-surface-400"
|
|
45
45
|
)}
|
|
46
46
|
>
|
|
47
47
|
{currentGroupName}
|
|
@@ -27,6 +27,8 @@ export function RebaseAuthGate({ children }: { children: React.ReactNode }) {
|
|
|
27
27
|
const registry = useRebaseRegistry();
|
|
28
28
|
const authController = useAuthController();
|
|
29
29
|
|
|
30
|
+
console.log("[AuthGate] initialLoading:", authController?.initialLoading, "user:", authController?.user?.email ?? null);
|
|
31
|
+
|
|
30
32
|
if (authController?.initialLoading) {
|
|
31
33
|
return <CircularProgressCenter size={"large"}/>;
|
|
32
34
|
}
|
|
@@ -10,7 +10,7 @@ import type { RebaseCMSConfig } from "@rebasepro/types";
|
|
|
10
10
|
* is auto-wired as a native feature (slots, provider, Studio view) without
|
|
11
11
|
* needing any external plugin.
|
|
12
12
|
*/
|
|
13
|
-
export function RebaseCMS({ collections, homePage, entityViews, entityActions, plugins, collectionEditor }: RebaseCMSConfig) {
|
|
13
|
+
export function RebaseCMS({ collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings }: RebaseCMSConfig) {
|
|
14
14
|
const dispatch = useRebaseRegistryDispatch();
|
|
15
15
|
|
|
16
16
|
useLayoutEffect(() => {
|
|
@@ -19,9 +19,10 @@ homePage,
|
|
|
19
19
|
entityViews,
|
|
20
20
|
entityActions,
|
|
21
21
|
plugins,
|
|
22
|
-
collectionEditor
|
|
22
|
+
collectionEditor,
|
|
23
|
+
navigationGroupMappings });
|
|
23
24
|
return () => dispatch.unregisterCMS();
|
|
24
|
-
}, [dispatch, collections, homePage, entityViews, entityActions, plugins, collectionEditor]);
|
|
25
|
+
}, [dispatch, collections, homePage, entityViews, entityActions, plugins, collectionEditor, navigationGroupMappings]);
|
|
25
26
|
|
|
26
27
|
return null;
|
|
27
28
|
}
|
|
@@ -69,11 +69,14 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
69
69
|
const userConfigPersistence = useBuildLocalConfigurationPersistence();
|
|
70
70
|
|
|
71
71
|
// ── Collection Editor resolution ──────────────────────────────────
|
|
72
|
+
// The collection editor is ALWAYS enabled when Studio is registered.
|
|
73
|
+
// The `collectionEditor` CMS config is for fine-tuning (readOnly, auth, etc.),
|
|
74
|
+
// not for opting-in. When omitted, the editor defaults to enabled
|
|
75
|
+
// (read-only in production).
|
|
72
76
|
const collectionEditorConfig = registry.cmsConfig?.collectionEditor;
|
|
73
|
-
const collectionEditorEnabled = Boolean(collectionEditorConfig);
|
|
77
|
+
const collectionEditorEnabled = Boolean(collectionEditorConfig) || Boolean(registry.studioConfig);
|
|
74
78
|
const collectionEditorOptions: CollectionEditorOptions | undefined = useMemo(() => {
|
|
75
|
-
if (!collectionEditorConfig) return
|
|
76
|
-
if (collectionEditorConfig === true) return {};
|
|
79
|
+
if (collectionEditorConfig === true || !collectionEditorConfig) return {};
|
|
77
80
|
return collectionEditorConfig;
|
|
78
81
|
}, [collectionEditorConfig]);
|
|
79
82
|
|
|
@@ -117,7 +120,6 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
117
120
|
return {
|
|
118
121
|
slug: "schema",
|
|
119
122
|
name: "Edit collections",
|
|
120
|
-
group: "Database",
|
|
121
123
|
icon: "LayoutList",
|
|
122
124
|
nestedRoutes: true,
|
|
123
125
|
view: (
|
|
@@ -138,6 +140,7 @@ export function RebaseNavigation({ children }: RebaseNavigationProps) {
|
|
|
138
140
|
plugins: registry.cmsConfig?.plugins ?? EMPTY_PLUGINS,
|
|
139
141
|
collections: collectionsBuilder,
|
|
140
142
|
views: devViews,
|
|
143
|
+
navigationGroupMappings: registry.cmsConfig?.navigationGroupMappings,
|
|
141
144
|
authController: context.authController!,
|
|
142
145
|
data: context.data,
|
|
143
146
|
collectionRegistryController,
|
|
@@ -83,6 +83,10 @@ export const RelationSelector = React.forwardRef<
|
|
|
83
83
|
// Track IDs that were set via local user interaction (onItemClick / handleClear / handleRemoveItem).
|
|
84
84
|
// When an incoming value change matches these IDs exactly, we skip async re-resolution.
|
|
85
85
|
const localSelectionIdsRef = useRef<string | null>(null);
|
|
86
|
+
// Snapshot of selected IDs captured when the popover opens.
|
|
87
|
+
// Used to sort the dropdown list so selected items appear at the top.
|
|
88
|
+
// Stays stable for the entire popover session so items don't jump around.
|
|
89
|
+
const pinnedIdsRef = useRef<Set<string> | null>(null);
|
|
86
90
|
|
|
87
91
|
const {
|
|
88
92
|
items: availableItems,
|
|
@@ -323,6 +327,7 @@ relation } as RelationItem;
|
|
|
323
327
|
newSelected = [item];
|
|
324
328
|
setIsPopoverOpen(false);
|
|
325
329
|
isPopoverOpenRef.current = false;
|
|
330
|
+
pinnedIdsRef.current = null;
|
|
326
331
|
}
|
|
327
332
|
setSelectedItems(newSelected);
|
|
328
333
|
// Mark this fingerprint so the resolution effect skips async work
|
|
@@ -348,11 +353,13 @@ relation } as RelationItem;
|
|
|
348
353
|
if (disabled) return;
|
|
349
354
|
// We control open manually; only allow opening attempts from Radix (e.g. trigger press)
|
|
350
355
|
if (next) {
|
|
356
|
+
// Capture current selection so we can pin those items to the top of the list
|
|
357
|
+
pinnedIdsRef.current = new Set(selectedItems.map(i => String(i.id)));
|
|
351
358
|
setIsPopoverOpen(true);
|
|
352
359
|
isPopoverOpenRef.current = true;
|
|
353
360
|
}
|
|
354
361
|
// Ignore close attempts here; outside click/Escape handled manually; single select closes explicitly on selection.
|
|
355
|
-
}, [disabled]);
|
|
362
|
+
}, [disabled, selectedItems]);
|
|
356
363
|
|
|
357
364
|
// Outside click + Escape handling (simple and reliable)
|
|
358
365
|
useEffect(() => {
|
|
@@ -367,12 +374,14 @@ relation } as RelationItem;
|
|
|
367
374
|
// Outside
|
|
368
375
|
setIsPopoverOpen(false);
|
|
369
376
|
isPopoverOpenRef.current = false;
|
|
377
|
+
pinnedIdsRef.current = null;
|
|
370
378
|
}
|
|
371
379
|
|
|
372
380
|
function handleKey(ev: KeyboardEvent) {
|
|
373
381
|
if (ev.key === "Escape") {
|
|
374
382
|
setIsPopoverOpen(false);
|
|
375
383
|
isPopoverOpenRef.current = false;
|
|
384
|
+
pinnedIdsRef.current = null;
|
|
376
385
|
}
|
|
377
386
|
}
|
|
378
387
|
|
|
@@ -389,6 +398,7 @@ relation } as RelationItem;
|
|
|
389
398
|
const closePopover = useCallback(() => {
|
|
390
399
|
setIsPopoverOpen(false);
|
|
391
400
|
isPopoverOpenRef.current = false;
|
|
401
|
+
pinnedIdsRef.current = null;
|
|
392
402
|
}, []);
|
|
393
403
|
|
|
394
404
|
const resolvedPlaceholder = placeholder || emptyPlaceholder || <EmptyValue className={"ml-2"}/>;
|
|
@@ -412,6 +422,11 @@ relation } as RelationItem;
|
|
|
412
422
|
setIsPopoverOpen(o => {
|
|
413
423
|
const next = !o;
|
|
414
424
|
isPopoverOpenRef.current = next;
|
|
425
|
+
if (next) {
|
|
426
|
+
pinnedIdsRef.current = new Set(selectedItems.map(i => String(i.id)));
|
|
427
|
+
} else {
|
|
428
|
+
pinnedIdsRef.current = null;
|
|
429
|
+
}
|
|
415
430
|
return next;
|
|
416
431
|
});
|
|
417
432
|
}}
|
|
@@ -589,7 +604,20 @@ relation } as RelationItem;
|
|
|
589
604
|
</CommandPrimitive.Empty>
|
|
590
605
|
)}
|
|
591
606
|
<CommandPrimitive.Group>
|
|
592
|
-
{
|
|
607
|
+
{(() => {
|
|
608
|
+
// Sort items so that initially-selected (pinned) items appear first.
|
|
609
|
+
// We use the snapshot taken when the popover opened so items don't
|
|
610
|
+
// jump around as the user checks/unchecks.
|
|
611
|
+
const pinned = pinnedIdsRef.current;
|
|
612
|
+
const sortedItems = pinned && pinned.size > 0
|
|
613
|
+
? [...availableItems].sort((a, b) => {
|
|
614
|
+
const aP = pinned.has(String(a.id)) ? 0 : 1;
|
|
615
|
+
const bP = pinned.has(String(b.id)) ? 0 : 1;
|
|
616
|
+
return aP - bP;
|
|
617
|
+
})
|
|
618
|
+
: availableItems;
|
|
619
|
+
return sortedItems;
|
|
620
|
+
})().map((item) => {
|
|
593
621
|
const isSelected = selectedItems.some(v => String(v.id) === String(item.id));
|
|
594
622
|
return (
|
|
595
623
|
<CommandPrimitive.Item
|
|
@@ -239,7 +239,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
239
239
|
return (
|
|
240
240
|
<SelectableTableContext.Provider
|
|
241
241
|
value={contextValue}>
|
|
242
|
-
<div className="h-full w-full flex flex-col bg-white dark:bg-surface-
|
|
242
|
+
<div className="h-full w-full flex flex-col bg-white dark:bg-surface-900"
|
|
243
243
|
ref={ref}>
|
|
244
244
|
|
|
245
245
|
<VirtualTable
|
|
@@ -264,7 +264,7 @@ export const SelectableTable = function SelectableTable<M extends Record<string,
|
|
|
264
264
|
checkFilterCombination={checkFilterCombination}
|
|
265
265
|
createFilterField={filterable ? createFilterField : undefined}
|
|
266
266
|
rowClassName={useCallback((entity: Entity<M>) => {
|
|
267
|
-
return highlightedRow?.(entity) ? "bg-surface-50
|
|
267
|
+
return highlightedRow?.(entity) ? "bg-surface-accent-50 dark:!bg-surface-accent-900" : "";
|
|
268
268
|
}, [highlightedRow])}
|
|
269
269
|
className="grow"
|
|
270
270
|
emptyComponent={emptyComponent}
|
|
@@ -7,7 +7,7 @@ import { Button, Container, Dialog, DialogActions, DialogContent, DialogTitle, I
|
|
|
7
7
|
import { MailIcon, KeyRoundIcon, PlusIcon, Trash2Icon, CopyIcon, CheckCircleIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
8
8
|
import { RoleChip } from "./RoleChip";
|
|
9
9
|
import { UserManagementDelegate, Role, UserCreationResult } from "@rebasepro/types";
|
|
10
|
-
import { ConfirmationDialog } from "@rebasepro/core";
|
|
10
|
+
import { ConfirmationDialog, BootstrapAdminBanner } from "@rebasepro/core";
|
|
11
11
|
|
|
12
12
|
const PAGE_SIZE = 25;
|
|
13
13
|
|
|
@@ -249,22 +249,7 @@ message: error instanceof Error ? error.message : t("error_resetting_password")
|
|
|
249
249
|
|
|
250
250
|
return (
|
|
251
251
|
<Container className="w-full flex flex-col py-4 gap-4" maxWidth={"6xl"}>
|
|
252
|
-
|
|
253
|
-
{!delegateLoading && !hasAdmin && !usersError && loggedInUser && bootstrapAdmin && (
|
|
254
|
-
<div className="bg-yellow-100 dark:bg-yellow-900 border border-yellow-400 dark:border-yellow-700 rounded p-4 flex items-center justify-between">
|
|
255
|
-
<div>
|
|
256
|
-
<Typography variant="label" className="text-yellow-800 dark:text-yellow-200">
|
|
257
|
-
{t("no_users_or_roles_defined")}
|
|
258
|
-
</Typography>
|
|
259
|
-
</div>
|
|
260
|
-
<Button
|
|
261
|
-
onClick={handleBootstrap}
|
|
262
|
-
disabled={bootstrapping}
|
|
263
|
-
>
|
|
264
|
-
{bootstrapping ? <CircularProgress size="small"/> : t("add_logged_user_as_admin")}
|
|
265
|
-
</Button>
|
|
266
|
-
</div>
|
|
267
|
-
)}
|
|
252
|
+
<BootstrapAdminBanner className="mb-4" />
|
|
268
253
|
|
|
269
254
|
<div className="flex items-center mt-12 mb-4 gap-4">
|
|
270
255
|
<Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
|