@rebasepro/admin 0.13.0 → 0.13.1-canary.g18cfeb7
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-CjXkWZ64.js → CollectionEditorDialog-CUFXevVg.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-CUFXevVg.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-DDUC7giL.js} +2 -2
- package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-DDUC7giL.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-vzS848Gf.js} +9 -13
- package/dist/RouterCollectionsStudioView-vzS848Gf.js.map +1 -0
- package/dist/collection_editor/serializable_types.d.ts +1 -0
- package/dist/collection_editor_ui.js +4 -4
- package/dist/components/AdditionalFieldValue.d.ts +25 -0
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -4
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +12 -0
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +55 -5
- package/dist/components/DetailViewBinding.d.ts +15 -0
- package/dist/components/DrawerNavigationGroup.d.ts +9 -6
- package/dist/components/DrawerNavigationItem.d.ts +16 -6
- package/dist/components/EditViewBinding.d.ts +30 -1
- package/dist/components/EntityDisplayHeader.d.ts +53 -0
- package/dist/components/EntityIdentityBar.d.ts +43 -1
- package/dist/components/EntityViewBinding.d.ts +60 -6
- package/dist/components/RebaseLayout.d.ts +1 -1
- package/dist/components/RebaseShell.d.ts +1 -1
- package/dist/components/RelationSelector.d.ts +8 -1
- package/dist/components/UserSelector.d.ts +8 -1
- package/dist/components/app/Scaffold.d.ts +9 -3
- package/dist/components/index.d.ts +2 -0
- package/dist/{export-J79rMoNj.js → export-5bl2EoSs.js} +2 -2
- package/dist/{export-J79rMoNj.js.map → export-5bl2EoSs.js.map} +1 -1
- package/dist/form/components/FieldBlock.d.ts +58 -4
- package/dist/form/components/FormRail.d.ts +13 -0
- package/dist/form/components/FormSections.d.ts +15 -1
- package/dist/form/components/useRailVisible.d.ts +5 -4
- package/dist/form/form_utils.d.ts +38 -1
- package/dist/{history-D7L_JlLO.js → history-BuKKAhoY.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-BuKKAhoY.js.map} +1 -1
- package/dist/hooks/useEntityDisplay.d.ts +102 -0
- package/dist/hooks/useEntityDisplayTitle.d.ts +18 -9
- package/dist/{import-BvYl--U4.js → import-BhdWs34H.js} +3 -3
- package/dist/{import-BvYl--U4.js.map → import-BhdWs34H.js.map} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +152 -57
- package/dist/index.js.map +1 -1
- package/dist/preview/compact_text.d.ts +23 -0
- package/dist/preview/components/BooleanPreview.d.ts +3 -1
- package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayOneOfPreview.d.ts +1 -1
- package/dist/preview/property_previews/ArrayPropertyPreview.d.ts +1 -1
- package/dist/preview/property_previews/MapPropertyPreview.d.ts +1 -1
- package/dist/preview/property_previews/StringPropertyPreview.d.ts +1 -1
- package/dist/routes/RebaseRoute.d.ts +1 -1
- package/dist/types/components/PropertyPreviewProps.d.ts +19 -0
- package/dist/util/number_format.d.ts +14 -0
- package/dist/util/previews.d.ts +9 -2
- package/dist/{util-GiwPgxnL.js → util-DNG7iK0w.js} +3756 -2300
- package/dist/util-DNG7iK0w.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/components/AdditionalFieldValue.tsx +68 -0
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +7 -8
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +2 -2
- package/src/components/CollectionTableBinding/column_utils.tsx +15 -6
- package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +2 -2
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +723 -348
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +38 -8
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +23 -36
- package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +33 -2
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +12 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +183 -64
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +19 -23
- package/src/components/DetailViewBinding.tsx +93 -56
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +44 -16
- package/src/components/EditViewBinding.tsx +215 -42
- package/src/components/EntityDisplayHeader.tsx +170 -0
- package/src/components/EntityIdentityBar.tsx +163 -23
- package/src/components/EntityInspector.tsx +24 -6
- package/src/components/EntityPreviewBinding.tsx +58 -18
- package/src/components/EntityViewBinding.tsx +342 -41
- package/src/components/InlineEntityPreview.tsx +10 -10
- package/src/components/RebaseLayout.tsx +2 -2
- package/src/components/RebaseShell.tsx +2 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
- package/src/components/RelationSelector.tsx +15 -4
- package/src/components/SideDialogs.tsx +15 -3
- package/src/components/SidePanelBinding.tsx +122 -66
- package/src/components/UserSelector.tsx +15 -4
- package/src/components/app/Scaffold.tsx +112 -10
- package/src/components/history/LastEditedByIndicator.tsx +7 -12
- package/src/components/index.ts +3 -0
- package/src/data_import/utils/data.ts +9 -1
- package/src/form/EntityForm.tsx +64 -52
- package/src/form/EntityFormBinding.tsx +24 -11
- package/src/form/PropertyFieldBinding.tsx +2 -2
- package/src/form/components/FieldBlock.tsx +80 -10
- package/src/form/components/FormRail.tsx +19 -2
- package/src/form/components/FormSections.tsx +32 -5
- package/src/form/components/StorageUploadProgress.tsx +9 -3
- package/src/form/components/useRailVisible.tsx +5 -4
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
- package/src/form/field_bindings/BinaryFieldBinding.tsx +10 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +13 -7
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +7 -1
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +7 -1
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +1 -1
- package/src/form/field_bindings/RelationFieldBinding.tsx +5 -2
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +2 -2
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +6 -4
- package/src/form/form_utils.ts +78 -0
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +29 -5
- package/src/hooks/useEntityDisplay.tsx +262 -0
- package/src/hooks/useEntityDisplayTitle.tsx +27 -38
- package/src/index.ts +9 -1
- package/src/preview/PropertyPreview.tsx +25 -5
- package/src/preview/compact_text.ts +68 -0
- package/src/preview/components/BooleanPreview.tsx +5 -2
- package/src/preview/components/StorageThumbnail.tsx +5 -2
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +26 -1
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +10 -1
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +10 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +52 -1
- package/src/preview/property_previews/NumberPropertyPreview.tsx +9 -1
- package/src/preview/property_previews/StringPropertyPreview.tsx +8 -5
- package/src/routes/RebaseRoute.tsx +62 -4
- package/src/types/components/PropertyPreviewProps.tsx +21 -0
- package/src/util/number_format.ts +46 -0
- package/src/util/previews.ts +8 -55
- package/src/util/property_utils.tsx +6 -1
- package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
- package/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -6,10 +6,10 @@ import { DetailViewBinding } from "../components/DetailViewBinding";
|
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
7
7
|
import { useNavigate } from "react-router";
|
|
8
8
|
import { CollectionViewBinding } from "../components/CollectionViewBinding/CollectionViewBinding";
|
|
9
|
-
import { NotFoundPage, useUserConfigurationPersistence, useComponentOverride, useNavigationBlocker } from "@rebasepro/app";
|
|
9
|
+
import { ErrorView, NotFoundPage, useUserConfigurationPersistence, useComponentOverride, useNavigationBlocker } from "@rebasepro/app";
|
|
10
10
|
import { resolveOpenEntityMode, resolveViewMode } from "../util/view_mode";
|
|
11
11
|
import { UnsavedChangesDialog } from "@rebasepro/app";
|
|
12
|
-
import { CircularProgressCenter } from "@rebasepro/ui";
|
|
12
|
+
import { CenteredView, CircularProgressCenter } from "@rebasepro/ui";
|
|
13
13
|
import { NavigationViewCollectionInternal, NavigationViewEntityCustomInternal, NavigationViewInternal } from "@rebasepro/app";
|
|
14
14
|
import { getNavigationEntriesFromPath } from "@rebasepro/app";
|
|
15
15
|
import { toArray } from "@rebasepro/utils";
|
|
@@ -94,7 +94,7 @@ export function RebaseRoute() {
|
|
|
94
94
|
if (!collectionRegistry.initialised) {
|
|
95
95
|
return <CircularProgressCenter/>;
|
|
96
96
|
}
|
|
97
|
-
return
|
|
97
|
+
return <UnresolvedCollectionView path={navigationEntries[0].slug}/>;
|
|
98
98
|
}
|
|
99
99
|
return <ResolvedCollectionView
|
|
100
100
|
key={`collection_view_${collection.slug}`}
|
|
@@ -117,7 +117,7 @@ export function RebaseRoute() {
|
|
|
117
117
|
if (!collectionRegistry.initialised) {
|
|
118
118
|
return <CircularProgressCenter/>;
|
|
119
119
|
}
|
|
120
|
-
return
|
|
120
|
+
return <UnresolvedCollectionView path={firstEntry.slug}/>;
|
|
121
121
|
}
|
|
122
122
|
return <ResolvedCollectionView
|
|
123
123
|
key={`collection_view_${collection.slug}`}
|
|
@@ -233,6 +233,7 @@ function EntityFullScreenRoute({
|
|
|
233
233
|
const urlController = useUrlController();
|
|
234
234
|
const navigate = useNavigate();
|
|
235
235
|
const location = useLocation();
|
|
236
|
+
const userConfigPersistence = useUserConfigurationPersistence();
|
|
236
237
|
|
|
237
238
|
// defaultValues may be carried via location.state when openNewDocument() is called
|
|
238
239
|
// for full-screen mode. We read it once on mount — after that, the form owns its state.
|
|
@@ -340,6 +341,30 @@ function EntityFullScreenRoute({
|
|
|
340
341
|
// Determine if this is a detail-view-first collection showing the view page
|
|
341
342
|
const isDetailMode = collection.defaultEntityAction === "view" && !isNew && !isCopy && entityId && !isEditRoute;
|
|
342
343
|
|
|
344
|
+
// A record is full screen either because its collection has no list to show
|
|
345
|
+
// beside it, or because the user folded that list away. Only the second is
|
|
346
|
+
// reversible, and it is reversible by dropping `#full` alone: this is the
|
|
347
|
+
// same URL the split route reads, so the same conditions decide it.
|
|
348
|
+
const splitListAvailable = !isNew && !isCopy && entityId !== undefined &&
|
|
349
|
+
navigationEntries[0]?.type === "collection" &&
|
|
350
|
+
(navigationEntries.length === 2 || navigationEntries.length === 3) &&
|
|
351
|
+
resolveOpenEntityMode({
|
|
352
|
+
collection,
|
|
353
|
+
viewMode: resolveViewMode({
|
|
354
|
+
collection,
|
|
355
|
+
search: location.search,
|
|
356
|
+
savedViewMode: userConfigPersistence?.getCollectionConfig(collection.slug)?.defaultViewMode as ViewMode | undefined
|
|
357
|
+
})
|
|
358
|
+
}) === "split";
|
|
359
|
+
|
|
360
|
+
const showList = splitListAvailable
|
|
361
|
+
? () => navigate({
|
|
362
|
+
pathname,
|
|
363
|
+
search: location.search,
|
|
364
|
+
hash: ""
|
|
365
|
+
})
|
|
366
|
+
: undefined;
|
|
367
|
+
|
|
343
368
|
if (isDetailMode) {
|
|
344
369
|
return <>
|
|
345
370
|
<DetailViewBinding
|
|
@@ -349,6 +374,7 @@ function EntityFullScreenRoute({
|
|
|
349
374
|
layout={"full_screen"}
|
|
350
375
|
path={collectionPath}
|
|
351
376
|
selectedTab={selectedTab ?? undefined}
|
|
377
|
+
onShowList={showList}
|
|
352
378
|
onEditClick={() => {
|
|
353
379
|
const editUrl = urlController.buildUrlCollectionPath(`${collectionPath}/${entityId}`) + "/edit";
|
|
354
380
|
navigate(editUrl + hash);
|
|
@@ -377,6 +403,7 @@ function EntityFullScreenRoute({
|
|
|
377
403
|
path={collectionPath}
|
|
378
404
|
copy={isCopy}
|
|
379
405
|
selectedTab={selectedTab ?? undefined}
|
|
406
|
+
onShowList={showList}
|
|
380
407
|
defaultValues={isNew ? defaultValues : undefined}
|
|
381
408
|
onValuesModified={(modified) => blocked.current = modified}
|
|
382
409
|
navigateBack={() => {
|
|
@@ -415,3 +442,34 @@ function EntityFullScreenRoute({
|
|
|
415
442
|
body={"You have unsaved changes in this entity."}/>
|
|
416
443
|
</>;
|
|
417
444
|
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Shown when a URL names a collection the registry cannot resolve.
|
|
448
|
+
*
|
|
449
|
+
* This used to be `return null`, which renders an empty pane inside the app
|
|
450
|
+
* chrome: the sidebar, the nav highlight and the breadcrumb all still work,
|
|
451
|
+
* because those read the collections array directly, while the view itself is
|
|
452
|
+
* blank. Nothing is thrown, and the only trace is a `console.debug` — so the
|
|
453
|
+
* panel looks like it lost its data rather than like it failed to find the
|
|
454
|
+
* collection, and there is no string to search for.
|
|
455
|
+
*
|
|
456
|
+
* A slug containing slashes went unresolvable this way and cost an afternoon to
|
|
457
|
+
* find. The lookup is fixed; this is the part that made it expensive.
|
|
458
|
+
*/
|
|
459
|
+
function UnresolvedCollectionView({ path }: { path: string }) {
|
|
460
|
+
// Warn, not debug: this is a route that renders nothing useful, and it
|
|
461
|
+
// should be visible at the console's default level.
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
console.warn(
|
|
464
|
+
`[rebase] No collection is registered for "${path}", so this route has nothing to render. ` +
|
|
465
|
+
"Check that the collection is in the collections array and that its slug matches the URL."
|
|
466
|
+
);
|
|
467
|
+
}, [path]);
|
|
468
|
+
|
|
469
|
+
return <CenteredView>
|
|
470
|
+
<ErrorView
|
|
471
|
+
title={"Collection not found"}
|
|
472
|
+
error={`Nothing is registered for "${path}".`}
|
|
473
|
+
tooltip={"The URL names a collection the panel does not know about. It may have been renamed or removed from the collections array, or the slug may not match the path."}/>
|
|
474
|
+
</CenteredView>;
|
|
475
|
+
}
|
|
@@ -61,4 +61,25 @@ export interface PropertyPreviewProps<P extends Property | Property, CustomProps
|
|
|
61
61
|
*/
|
|
62
62
|
textOnly?: boolean;
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The preview has one line and cannot grow — it fills a slot in a card, a
|
|
66
|
+
* chip, or a table cell of fixed height.
|
|
67
|
+
*
|
|
68
|
+
* Properties whose natural rendering is a block (Markdown, multi-line text,
|
|
69
|
+
* maps, arrays) render a one-line stand-in instead: an opening excerpt, or
|
|
70
|
+
* a count. Left unset, {@link PropertyPreview} infers it from the entity
|
|
71
|
+
* preview nesting depth, which is where the constraint actually comes from;
|
|
72
|
+
* pass it explicitly only to force the compact form somewhere the nesting
|
|
73
|
+
* does not say so.
|
|
74
|
+
*/
|
|
75
|
+
compact?: boolean;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The caller has already labelled this value, so the preview must not
|
|
79
|
+
* repeat the property name. A boolean is the one that does: it renders its
|
|
80
|
+
* name beside the checkbox, which reads as a caption in a table cell and as
|
|
81
|
+
* a stutter under a field label — "VIP" over a checkbox saying "VIP".
|
|
82
|
+
*/
|
|
83
|
+
hideLabel?: boolean;
|
|
84
|
+
|
|
64
85
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { NumberFormatOptions } from "@rebasepro/admin-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write a number out for reading, per a property's declared `admin.format`.
|
|
5
|
+
*
|
|
6
|
+
* Nothing is inferred here. With no options the number is returned as it came
|
|
7
|
+
* out of the database — `String(value)`, not a locale-grouped rendering — so a
|
|
8
|
+
* panel that declares nothing shows exactly the stored value, and an author who
|
|
9
|
+
* wants thousands separators asks for them.
|
|
10
|
+
*
|
|
11
|
+
* `Intl.NumberFormat` throws on a bad currency code or an out-of-range fraction
|
|
12
|
+
* count, and a record must not blank because one property is misconfigured, so a
|
|
13
|
+
* failure falls back to the raw value.
|
|
14
|
+
*/
|
|
15
|
+
export function formatNumber(
|
|
16
|
+
value: number,
|
|
17
|
+
options: NumberFormatOptions | undefined,
|
|
18
|
+
fallbackLocale?: string
|
|
19
|
+
): string {
|
|
20
|
+
|
|
21
|
+
if (!options) return String(value);
|
|
22
|
+
|
|
23
|
+
// A currency code is the whole declaration in the common case: writing
|
|
24
|
+
// `{ currency: "EUR" }` and getting a bare `185.11` because `style` was
|
|
25
|
+
// omitted is a trap, and there is nothing else `currency` could mean.
|
|
26
|
+
const style = options.style ?? (options.currency ? "currency" : "decimal");
|
|
27
|
+
|
|
28
|
+
if (style === "currency" && !options.currency) {
|
|
29
|
+
// `Intl` throws on this rather than picking a currency, which is the
|
|
30
|
+
// right call — there is no sensible default — but the record still has
|
|
31
|
+
// to render.
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return new Intl.NumberFormat(options.locale ?? fallbackLocale ?? undefined, {
|
|
37
|
+
style,
|
|
38
|
+
currency: options.currency,
|
|
39
|
+
minimumFractionDigits: options.minimumFractionDigits,
|
|
40
|
+
maximumFractionDigits: options.maximumFractionDigits,
|
|
41
|
+
notation: options.notation
|
|
42
|
+
}).format(value);
|
|
43
|
+
} catch {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/util/previews.ts
CHANGED
|
@@ -1,64 +1,17 @@
|
|
|
1
1
|
import type { Property } from "@rebasepro/types";
|
|
2
2
|
import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
3
|
-
import { AuthController } from "@rebasepro/admin-types";
|
|
4
|
-
import { isPropertyBuilder } from "@rebasepro/common";
|
|
5
3
|
import { getTitlePropertyKey, getTitlePropertyKeyForValues } from "@rebasepro/app";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
function isHiddenProperty(property: Property | undefined): boolean {
|
|
9
|
-
if (!property) return false;
|
|
10
|
-
return Boolean(property.admin?.hideFromCollection);
|
|
11
|
-
}
|
|
4
|
+
import { isPropertyBuilder } from "@rebasepro/common";
|
|
12
5
|
|
|
13
6
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* Which properties fill a preview surface, best first.
|
|
8
|
+
*
|
|
9
|
+
* The implementation lives in `@rebasepro/app` so that the admin package and
|
|
10
|
+
* the app package cannot disagree about what a record looks like in a card —
|
|
11
|
+
* they had drifted, and only one of the two copies had learned to keep
|
|
12
|
+
* relations out of the list.
|
|
17
13
|
*/
|
|
18
|
-
|
|
19
|
-
if (!property) return false;
|
|
20
|
-
// Single string with storage config
|
|
21
|
-
if (property.type === "string" && property.storage) return true;
|
|
22
|
-
// String displayed as image URL
|
|
23
|
-
if (property.type === "string" && property.admin?.urlPreview === "image") return true;
|
|
24
|
-
// Array whose inner element has storage config or image URL
|
|
25
|
-
if (property.type === "array" && property.of && !Array.isArray(property.of)) {
|
|
26
|
-
const inner = property.of;
|
|
27
|
-
if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return true;
|
|
28
|
-
}
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function getEntityPreviewKeys(
|
|
33
|
-
authController: AuthController,
|
|
34
|
-
targetCollection: AdminCollection<any>,
|
|
35
|
-
fields: Record<string, PropertyConfig>,
|
|
36
|
-
previewProperties?: string[],
|
|
37
|
-
limit = 3) {
|
|
38
|
-
const allProperties = Object.keys(targetCollection.properties);
|
|
39
|
-
let listProperties = previewProperties?.filter(p => allProperties.includes(p as string));
|
|
40
|
-
if (!listProperties && targetCollection.previewProperties) {
|
|
41
|
-
listProperties = targetCollection.previewProperties?.filter(p => allProperties.includes(p as string));
|
|
42
|
-
}
|
|
43
|
-
if (listProperties && listProperties.length > 0) {
|
|
44
|
-
return listProperties;
|
|
45
|
-
} else {
|
|
46
|
-
const hasExplicitOrder = !!(targetCollection.propertiesOrder as string[] | undefined);
|
|
47
|
-
listProperties = (targetCollection.propertiesOrder as string[]) || allProperties;
|
|
48
|
-
return listProperties
|
|
49
|
-
.filter(key => {
|
|
50
|
-
const prop = targetCollection.properties[key];
|
|
51
|
-
const isIdProp = prop && typeof prop === "object" && "isId" in prop && Boolean((prop as { isId?: boolean }).isId);
|
|
52
|
-
return !isIdProp && key !== "id";
|
|
53
|
-
})
|
|
54
|
-
.filter(key => {
|
|
55
|
-
const property = targetCollection.properties[key];
|
|
56
|
-
return property && !isPropertyBuilder(property) && !isReferenceProperty(property) && !isHiddenProperty(property)
|
|
57
|
-
&& !isStorageProperty(property as Property)
|
|
58
|
-
&& (hasExplicitOrder || !isRelationProperty(property));
|
|
59
|
-
}).slice(0, limit);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
14
|
+
export { getEntityPreviewKeys } from "@rebasepro/app";
|
|
62
15
|
|
|
63
16
|
/**
|
|
64
17
|
* The property that fills the title slot for a collection.
|
|
@@ -159,8 +159,13 @@ export function getResolvedPropertyInPath(properties: Record<string, Property>,
|
|
|
159
159
|
}
|
|
160
160
|
if (path.includes(".")) {
|
|
161
161
|
const pathSegments = path.split(".");
|
|
162
|
+
// A path whose root is not a property at all is simply not found.
|
|
163
|
+
// Reading `.type` off the miss threw a TypeError instead, which
|
|
164
|
+
// turned one unresolvable column key into a crash for whatever was
|
|
165
|
+
// walking the properties — see `propertiesToColumns`, where that is
|
|
166
|
+
// the whole table. `getPropertyInPath` above already guards this.
|
|
162
167
|
const childProperty = properties[pathSegments[0]];
|
|
163
|
-
if (childProperty
|
|
168
|
+
if (childProperty?.type === "map" && childProperty.properties) {
|
|
164
169
|
return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."))
|
|
165
170
|
}
|
|
166
171
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RouterCollectionsStudioView-BsLTNoqX.js","names":[],"sources":["../src/collection_editor/ui/collection_editor/CollectionStudioView.tsx","../src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx","../src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx"],"sourcesContent":["\nimport { useUnsavedChangesDialog, UnsavedChangesDialog } from \"@rebasepro/app\";\nimport * as React from \"react\";\nimport { useState } from \"react\";\nimport { Button, PlusIcon, Typography } from \"@rebasepro/ui\";\nimport { CollectionEditorDialogProps } from \"./CollectionEditorDialog\";\nimport { AIModifiedPathsProvider } from \"./AIModifiedPathsContext\";\nimport { CollectionEditor } from \"./CollectionEditorDialog\";\nimport type { AdminCollection } from \"@rebasepro/admin-types\";\n\nexport type CollectionStudioViewProps = Omit<CollectionEditorDialogProps, \"open\" | \"isNewCollection\" | \"editedCollectionId\" | \"handleClose\" | \"handleCancel\"> & {\n collectionId?: string | \"new\";\n\n /**\n * Called after a successful save.\n * Receives the saved collection (or undefined if dismissed without saving).\n * When not provided, save completes silently.\n */\n onSave?: (collection?: AdminCollection) => void;\n\n /**\n * Called when the user cancels editing.\n * When not provided, cancel is a no-op.\n */\n onCancel?: () => void;\n};\n\nexport function CollectionStudioView({ collectionId, onSave, onCancel, ...props }: CollectionStudioViewProps) {\n\n // Form state from the editor\n const [formDirty, setFormDirty] = useState<boolean>(false);\n const [cancelRequested, setCancelRequested] = useState<boolean>(false);\n\n const { dialogProps, triggerDialog } = useUnsavedChangesDialog(\n formDirty,\n () => setFormDirty(false)\n );\n\n const activeCollectionId = collectionId;\n\n const handleCancelClick = () => {\n if (!formDirty) {\n onCancel?.();\n } else {\n setCancelRequested(true);\n triggerDialog();\n }\n };\n\n return (\n <div className=\"flex-grow flex flex-col h-full w-full bg-surface-50 dark:bg-surface-800\">\n <AIModifiedPathsProvider>\n {activeCollectionId ? (\n <CollectionEditor\n key={activeCollectionId}\n {...props}\n fullScreen={true}\n open={true}\n isNewCollection={activeCollectionId === \"new\"}\n editedCollectionId={activeCollectionId !== \"new\" ? activeCollectionId : undefined}\n handleCancel={handleCancelClick}\n handleClose={(savedCollection) => {\n setFormDirty(false);\n if (savedCollection) {\n setTimeout(() => {\n onSave?.(savedCollection);\n }, 0);\n }\n }}\n setFormDirty={setFormDirty}\n />\n ) : (\n <div className=\"flex-grow flex flex-col items-center justify-center h-full gap-4\">\n <Typography variant=\"label\">\n Select a collection or create a new one\n </Typography>\n <Button\n disabled={props.configController?.readOnly}\n onClick={() => onSave?.()}\n >\n <PlusIcon/>\n Add new collection\n </Button>\n </div>\n )}\n\n <UnsavedChangesDialog\n {...dialogProps}\n handleOk={() => {\n dialogProps.handleOk();\n if (cancelRequested) {\n onCancel?.();\n setCancelRequested(false);\n }\n }}\n handleCancel={() => {\n dialogProps.handleCancel();\n setCancelRequested(false);\n }}\n />\n </AIModifiedPathsProvider>\n </div>\n );\n}\n","\nimport { IconForView } from \"@rebasepro/app\";\nimport React, { useState, useEffect } from \"react\";\nimport {\n Button,\n cls,\n defaultBorderMixin,\n IconButton,\n iconSize,\n PlusIcon,\n ResizablePanels,\n Tooltip,\n Typography\n} from \"@rebasepro/ui\";\n\nimport { CollectionsConfigController } from \"../../types/config_controller\";\nimport { CollectionStudioView } from \"./CollectionStudioView\";\nimport type { CollectionEditorExtensionProps } from \"../../extensibility_types\";\nimport type { AdminCollection } from \"@rebasepro/admin-types\";\n\nexport interface CollectionsStudioViewProps extends CollectionEditorExtensionProps {\n configController: CollectionsConfigController;\n\n /**\n * Collections to show in the sidebar.\n * When provided, overrides the collections from `configController`.\n * Use this to control exactly which collections the editor displays.\n */\n collections?: AdminCollection[];\n\n /**\n * Controlled active collection ID.\n * When provided together with `onActiveCollectionChange`,\n * navigation is fully external — no internal state is used.\n */\n activeCollectionId?: string;\n\n /**\n * Called when the user clicks a collection, \"new\", or navigates.\n * When provided, the component is fully controlled (no internal state).\n * When not provided, the component manages selection via internal state.\n *\n * Pass `\"new\"` to create a new collection. Pass `undefined` to deselect.\n */\n onActiveCollectionChange?: (collectionId: string | undefined) => void;\n}\n\nexport function CollectionsStudioView({\n configController,\n collections: collectionsProp,\n activeCollectionId: controlledActiveId,\n onActiveCollectionChange,\n propertyTypePresets,\n hiddenPropertyTypes,\n renderExtraPropertyFields,\n renderExtraCollectionFields,\n visibleTabs,\n standalone,\n}: CollectionsStudioViewProps) {\n\n // ── Navigation state ────────────────────────────────────────────────\n // If onActiveCollectionChange is provided, the component is controlled.\n // Otherwise, use internal state.\n const [internalActiveId, setInternalActiveId] = useState<string | undefined>(undefined);\n const isControlled = onActiveCollectionChange !== undefined;\n const activeCollectionId = isControlled ? controlledActiveId : internalActiveId;\n\n const setActiveCollectionId = (id: string | undefined) => {\n if (isControlled) {\n onActiveCollectionChange?.(id);\n } else {\n setInternalActiveId(id);\n }\n };\n\n // ── Collections list ────────────────────────────────────────────────\n const collections = collectionsProp ?? configController.collections ?? [];\n\n // ── Sidebar sizing ──────────────────────────────────────────────────\n const [sidebarSize, setSidebarSize] = useState(() => {\n try {\n const saved = localStorage.getItem(\"rebase_collections_editor_sidebar_size\");\n return saved !== null ? parseFloat(saved) : 25;\n } catch (e) {\n return 25;\n }\n });\n\n useEffect(() => {\n try {\n localStorage.setItem(\"rebase_collections_editor_sidebar_size\", sidebarSize.toString());\n } catch (e) {\n // ignore local storage error\n }\n }, [sidebarSize]);\n\n return (\n <div className=\"flex h-full w-full bg-surface-50 dark:bg-surface-800 overflow-hidden text-text-primary dark:text-text-primary-dark\">\n <ResizablePanels\n orientation=\"horizontal\"\n panelSizePercent={sidebarSize}\n onPanelSizeChange={setSidebarSize}\n minPanelSizePx={220}\n firstPanel={\n <div className={cls(\"flex flex-col h-full w-full bg-surface-50 dark:bg-surface-800 border-r\", defaultBorderMixin)}>\n <div className={cls(\"flex items-center justify-between px-3 py-2 border-b bg-surface-50 dark:bg-surface-900 min-h-[48px]\", defaultBorderMixin)}>\n <Typography variant=\"caption\" className=\"font-semibold text-[11px] uppercase tracking-wider text-surface-400 dark:text-surface-400\">\n Collections\n </Typography>\n <Tooltip title={configController.readOnly ? configController.readOnlyReason || \"Read only\" : \"Add collection\"}>\n <div>\n <IconButton\n size=\"small\"\n disabled={configController.readOnly}\n onClick={() => setActiveCollectionId(\"new\")}\n className={activeCollectionId === \"new\" ? \"text-primary dark:text-primary-dark\" : \"text-text-secondary dark:text-text-secondary-dark\"}\n >\n <PlusIcon size={iconSize.smallest}/>\n </IconButton>\n </div>\n </Tooltip>\n </div>\n\n <div className=\"flex-grow overflow-y-auto w-full no-scrollbar p-2 space-y-0.5\">\n {collections.length === 0 && (\n <div className=\"p-4 text-center\">\n <Typography variant=\"caption\" className=\"text-text-disabled dark:text-text-disabled-dark italic\">\n No collections yet.\n </Typography>\n </div>\n )}\n {collections.map((collection) => {\n const collectionKey = collection.slug;\n const isSelected = activeCollectionId === collectionKey;\n return (\n <div\n key={collectionKey}\n onClick={() => setActiveCollectionId(collectionKey)}\n className={cls(\n \"flex items-center gap-2.5 px-3 h-[30px] cursor-pointer rounded-lg text-[13px] font-medium transition-colors\",\n isSelected\n ? \"bg-primary/8 text-primary dark:bg-primary/10 dark:text-primary-light font-semibold\"\n : \"hover:bg-primary/5 dark:hover:bg-primary/5 text-surface-700 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white\"\n )}\n >\n <IconForView collectionOrView={collection} size={\"smallest\"} className={cls(\n isSelected\n ? \"text-primary dark:text-primary-light\"\n : \"text-surface-500 dark:text-text-secondary-dark\"\n )}/>\n <span className=\"truncate flex-1\">\n {collection.name || collection.slug}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n }\n secondPanel={\n <div className=\"flex-grow flex flex-col min-w-0 h-full w-full\">\n {/* We use key to force unmount when switching collections, preventing stale state */}\n {activeCollectionId ? (\n <CollectionStudioView\n key={activeCollectionId}\n configController={configController}\n collectionId={activeCollectionId}\n onSave={(savedCollection) => {\n // After creating a new collection, switch to it\n if (activeCollectionId === \"new\" && savedCollection?.slug) {\n setActiveCollectionId(savedCollection.slug);\n }\n }}\n onCancel={() => setActiveCollectionId(undefined)}\n propertyTypePresets={propertyTypePresets}\n hiddenPropertyTypes={hiddenPropertyTypes}\n renderExtraPropertyFields={renderExtraPropertyFields}\n renderExtraCollectionFields={renderExtraCollectionFields}\n visibleTabs={visibleTabs}\n standalone={standalone}\n />\n ) : (\n <div className=\"flex flex-col items-center justify-center h-full gap-4\">\n <Typography variant=\"label\">\n Select a collection or create a new one to start editing\n </Typography>\n <Button\n disabled={configController.readOnly}\n onClick={() => setActiveCollectionId(\"new\")}\n >\n <PlusIcon/>\n Add new collection\n </Button>\n </div>\n )}\n </div>\n }\n />\n </div>\n );\n}\n","/**\n * Router-aware wrapper around CollectionsStudioView.\n *\n * This component provides the react-router-based navigation behavior\n * that Rebase Studio expects: the active collection is derived from the\n * URL, and clicking a collection navigates via react-router.\n *\n * External consumers should use `CollectionsStudioView` directly\n * (which has no react-router dependency).\n */\nimport React from \"react\";\nimport { useLocation, useNavigate } from \"react-router\";\nimport { useUrlController } from \"../../_cms_internals\";\nimport { CollectionsStudioView, CollectionsStudioViewProps } from \"./CollectionsStudioView\";\n\nexport type RouterCollectionsStudioViewProps = Omit<CollectionsStudioViewProps, \"activeCollectionId\" | \"onActiveCollectionChange\">;\n\nexport function RouterCollectionsStudioView(props: RouterCollectionsStudioViewProps) {\n const navigate = useNavigate();\n const urlController = useUrlController();\n const location = useLocation();\n\n // Determine the active collection from the URL segment after \"schema/\"\n const basePath = urlController.buildAppUrlPath(\"schema\");\n const relativePath = location.pathname.replace(basePath, \"\").replace(/^\\//, \"\");\n const activeCollectionId = relativePath.split(\"/\")[0] || undefined;\n\n return (\n <CollectionsStudioView\n {...props}\n activeCollectionId={activeCollectionId}\n onActiveCollectionChange={(id) => {\n if (id) {\n navigate(urlController.buildAppUrlPath(`schema/${id}`));\n } else {\n navigate(urlController.buildAppUrlPath(\"schema\"));\n }\n }}\n />\n );\n}\n"],"mappings":"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"}
|