@rebasepro/admin 0.13.0 → 0.13.1-canary.g06dbe5b
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-C3DbFNqp.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-C3DbFNqp.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-DFH8rXRN.js} +5 -5
- package/dist/PropertyEditView-DFH8rXRN.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-DiUDOu4b.js} +9 -13
- package/dist/RouterCollectionsStudioView-DiUDOu4b.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/ClearFilterSortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/CollectionViewBinding.d.ts +16 -0
- package/dist/components/CollectionViewBinding/EntityCardBinding.d.ts +3 -1
- package/dist/components/CollectionViewBinding/SearchExplanation.d.ts +48 -0
- package/dist/components/CollectionViewBinding/SearchHighlight.d.ts +101 -0
- package/dist/components/CollectionViewBinding/SlotValue.d.ts +63 -0
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- 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/data_export/export/export.d.ts +7 -0
- package/dist/data_export/export/fetch_export_data.d.ts +48 -0
- package/dist/data_export/export/index.d.ts +1 -0
- package/dist/data_import/utils/csv.d.ts +30 -0
- package/dist/data_import/utils/index.d.ts +2 -0
- package/dist/data_import/utils/save_entities.d.ts +38 -0
- package/dist/data_import/utils/transforms.d.ts +9 -0
- package/dist/{export-J79rMoNj.js → export-Bd5i6Uvm.js} +175 -39
- package/dist/export-Bd5i6Uvm.js.map +1 -0
- 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-D9R6LUrH.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-D9R6LUrH.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-pt_j-0G3.js} +6 -5
- package/dist/import-pt_j-0G3.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +158 -60
- 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/view_constants.d.ts +24 -0
- package/dist/util/view_mode.d.ts +18 -0
- package/dist/{util-GiwPgxnL.js → util-nX_-JRuF.js} +3984 -1774
- package/dist/util-nX_-JRuF.js.map +1 -0
- package/package.json +10 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +1 -1
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +5 -2
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +10 -3
- package/src/components/AdditionalFieldValue.tsx +68 -0
- package/src/components/ClearFilterSortButton.tsx +16 -7
- 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/CollectionTableBinding/table_bindings.tsx +7 -1
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +26 -27
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +822 -354
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +51 -12
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +51 -39
- package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
- package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
- package/src/components/CollectionViewBinding/SlotValue.tsx +156 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -7
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +28 -0
- package/src/components/CollectionViewBinding/SplitListView.tsx +47 -7
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +23 -6
- 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 +109 -62
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +44 -16
- package/src/components/EditViewBinding.tsx +233 -50
- 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 +344 -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 +35 -10
- package/src/components/SideDialogs.tsx +15 -3
- package/src/components/SidePanelBinding.tsx +122 -66
- package/src/components/UserSelector.tsx +67 -13
- 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_export/export/ExportCollectionAction.tsx +101 -48
- package/src/data_export/export/export.ts +63 -16
- package/src/data_export/export/fetch_export_data.ts +90 -0
- package/src/data_export/export/index.ts +1 -0
- package/src/data_import/components/ImportSaveInProgress.tsx +35 -36
- package/src/data_import/import/ImportCollectionAction.tsx +13 -2
- package/src/data_import/utils/csv.ts +153 -0
- package/src/data_import/utils/data.ts +16 -2
- package/src/data_import/utils/file_to_json.ts +51 -5
- package/src/data_import/utils/index.ts +2 -0
- package/src/data_import/utils/save_entities.ts +138 -0
- package/src/data_import/utils/transforms.ts +23 -0
- package/src/form/EntityForm.tsx +74 -53
- 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 +21 -2
- 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/ReferencePreview.tsx +5 -8
- package/src/preview/components/RelationPreview.tsx +8 -7
- 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/navigation_utils.ts +4 -2
- package/src/util/number_format.ts +46 -0
- package/src/util/previews.ts +8 -55
- package/src/util/property_utils.tsx +6 -1
- package/src/util/view_constants.ts +27 -0
- package/src/util/view_mode.ts +36 -0
- package/dist/PropertyEditView-W8I3YV89.js.map +0 -1
- package/dist/RouterCollectionsStudioView-BsLTNoqX.js.map +0 -1
- package/dist/export-J79rMoNj.js.map +0 -1
- package/dist/import-BvYl--U4.js.map +0 -1
- package/dist/util-GiwPgxnL.js.map +0 -1
|
@@ -74,7 +74,7 @@ export function CollectionViewActions<M extends Record<string, unknown>>({
|
|
|
74
74
|
size="small"
|
|
75
75
|
variant="filled"
|
|
76
76
|
color="primary">
|
|
77
|
-
|
|
77
|
+
{t("add_specific", { name: collection.singularName ?? collection.name })}
|
|
78
78
|
</Button>
|
|
79
79
|
: <Button
|
|
80
80
|
id={`add_entity_${path}`}
|
|
@@ -28,6 +28,7 @@ import { ReferencePreview } from "../../preview";
|
|
|
28
28
|
import {
|
|
29
29
|
CollectionScopeProvider,
|
|
30
30
|
OnColumnResizeParams,
|
|
31
|
+
getRedundantChildViewColumnIds,
|
|
31
32
|
useAnalyticsController,
|
|
32
33
|
useAuthController,
|
|
33
34
|
useColumnIds,
|
|
@@ -281,8 +282,15 @@ const CollectionViewBindingInner = React.memo(
|
|
|
281
282
|
useEffect(() => {
|
|
282
283
|
if (!urlView && viewMode !== defaultViewMode) {
|
|
283
284
|
// View came from saved config but URL doesn't have it - update URL without push
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
// Built from `window.location.search`, not React Router's
|
|
286
|
+
// `prev`. The table controller writes the filter, sort and
|
|
287
|
+
// search params with `history.replaceState`, which React Router
|
|
288
|
+
// never observes — so `prev` is a stale snapshot, and merging
|
|
289
|
+
// into it silently dropped whatever it had not seen. Switching
|
|
290
|
+
// view mode after typing a search wiped `search=` from the URL,
|
|
291
|
+
// and the next reload came back unfiltered.
|
|
292
|
+
setSearchParams(() => {
|
|
293
|
+
const next = new URLSearchParams(window.location.search);
|
|
286
294
|
next.set(VIEW_MODE_PARAM, viewMode);
|
|
287
295
|
return next;
|
|
288
296
|
}, { replace: true });
|
|
@@ -294,8 +302,10 @@ const CollectionViewBindingInner = React.memo(
|
|
|
294
302
|
setViewModeState(newMode);
|
|
295
303
|
|
|
296
304
|
// Update URL with __view param via React Router
|
|
297
|
-
setSearchParams((
|
|
298
|
-
|
|
305
|
+
setSearchParams(() => {
|
|
306
|
+
// See the note above: the live URL is the only writer both
|
|
307
|
+
// this and the table controller agree on.
|
|
308
|
+
const next = new URLSearchParams(window.location.search);
|
|
299
309
|
if (newMode === defaultViewMode) {
|
|
300
310
|
next.delete(VIEW_MODE_PARAM);
|
|
301
311
|
} else {
|
|
@@ -687,7 +697,16 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
687
697
|
// Each child view gets a column that jumps to its tab. Keyed by the
|
|
688
698
|
// view's collection slug, which is the relation key — two relations
|
|
689
699
|
// to the same target used to collapse onto one column id here.
|
|
690
|
-
|
|
700
|
+
//
|
|
701
|
+
// Except where the relation was declared as a property and so already
|
|
702
|
+
// has a column showing the child rows: the button then repeated that
|
|
703
|
+
// column's heading verbatim. Filtered here as well as in
|
|
704
|
+
// `useColumnIds` so no id is displayed without a delegate to build it,
|
|
705
|
+
// and so the column picker does not offer the one back.
|
|
706
|
+
const redundant = getRedundantChildViewColumnIds(collectionWithLocalOrder);
|
|
707
|
+
const subcollectionsList = getEntityChildViews(collection)
|
|
708
|
+
.map(v => v.collection as AdminCollection)
|
|
709
|
+
.filter(child => !redundant.has(getSubcollectionColumnId(child)));
|
|
691
710
|
const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = subcollectionsList.map((subcollection: AdminCollection) => {
|
|
692
711
|
return {
|
|
693
712
|
key: getSubcollectionColumnId(subcollection),
|
|
@@ -720,7 +739,7 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
720
739
|
...(collection.additionalFields ?? EMPTY_ARRAY),
|
|
721
740
|
...subcollectionColumns
|
|
722
741
|
];
|
|
723
|
-
}, [collection, path, sidePanelController]);
|
|
742
|
+
}, [collection, collectionWithLocalOrder, path, sidePanelController]);
|
|
724
743
|
|
|
725
744
|
const updateLastDeleteTimestamp = useCallback(() => {
|
|
726
745
|
setLastDeleteTimestamp(Date.now());
|
|
@@ -823,6 +842,7 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
823
842
|
collection={collection}
|
|
824
843
|
filter={tableController.filterValues}
|
|
825
844
|
sortBy={tableController.sortBy}
|
|
845
|
+
searchString={tableController.searchString}
|
|
826
846
|
onCountChange={setDocsCount}
|
|
827
847
|
/>;
|
|
828
848
|
|
|
@@ -1126,13 +1146,18 @@ parentEntityIds,
|
|
|
1126
1146
|
|
|
1127
1147
|
{countFetcher}
|
|
1128
1148
|
|
|
1129
|
-
{
|
|
1149
|
+
{/* A plugin's error view takes priority over the built-in
|
|
1150
|
+
banner, the same precedence the empty state uses. This was
|
|
1151
|
+
computed above and then rendered nowhere, so `collection.error`
|
|
1152
|
+
— a declared, documented slot with its own props interface —
|
|
1153
|
+
did nothing at all, and a plugin author had no way to tell. */}
|
|
1154
|
+
{pluginErrorView ?? (tableController.dataLoadingError && tableController.data.length > 0 && (
|
|
1130
1155
|
<div className="flex items-center gap-4 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 flex-shrink-0">
|
|
1131
1156
|
<Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
|
|
1132
1157
|
<strong>Warning:</strong> {tableController.dataLoadingError.message || "Failed to update data."}
|
|
1133
1158
|
</Typography>
|
|
1134
1159
|
</div>
|
|
1135
|
-
)}
|
|
1160
|
+
))}
|
|
1136
1161
|
|
|
1137
1162
|
{/* When isSplitLayout, SplitListView is ALWAYS mounted — regardless
|
|
1138
1163
|
of viewMode. The toolbar + current view live in the left panel;
|
|
@@ -1302,17 +1327,26 @@ function DefaultCollectionEmptyState({
|
|
|
1302
1327
|
*/
|
|
1303
1328
|
const inflightCountRequests = new Map<string, Promise<number>>();
|
|
1304
1329
|
|
|
1305
|
-
function EntitiesCount({
|
|
1330
|
+
export function EntitiesCount({
|
|
1306
1331
|
path,
|
|
1307
1332
|
collection,
|
|
1308
1333
|
filter,
|
|
1309
1334
|
sortBy,
|
|
1335
|
+
searchString,
|
|
1310
1336
|
onCountChange
|
|
1311
1337
|
}: {
|
|
1312
1338
|
path: string,
|
|
1313
1339
|
collection: AdminCollection,
|
|
1314
1340
|
filter?: FilterValues<any>,
|
|
1315
1341
|
sortBy?: [string, "asc" | "desc"],
|
|
1342
|
+
/**
|
|
1343
|
+
* Required, not optional. The term sits in the same scope as the element
|
|
1344
|
+
* that mounts this and is passed to the toolbar and the empty state beside
|
|
1345
|
+
* it — it was simply not passed here, so a searched list showed the count
|
|
1346
|
+
* of the unsearched collection. A required prop is what stops the next
|
|
1347
|
+
* caller doing the same; there is no useful "count without the search".
|
|
1348
|
+
*/
|
|
1349
|
+
searchString: string | undefined,
|
|
1316
1350
|
onCountChange?: (count: number | null | undefined) => void,
|
|
1317
1351
|
}) {
|
|
1318
1352
|
|
|
@@ -1344,12 +1378,17 @@ function EntitiesCount({
|
|
|
1344
1378
|
const orderByParams: [string, "asc" | "desc"] | undefined = sortByProperty && currentSort ? [String(sortByProperty), currentSort] : undefined;
|
|
1345
1379
|
|
|
1346
1380
|
// Deduplicate inflight count requests (e.g. React StrictMode double-mount)
|
|
1347
|
-
|
|
1381
|
+
// The search term is part of the query, so it is part of the key. The
|
|
1382
|
+
// cache is module-level and outlives an unmount, so a key that omits it
|
|
1383
|
+
// does not merely lose precision — it answers one search with a
|
|
1384
|
+
// different search's total.
|
|
1385
|
+
const cacheKey = `${path}|${filterKey}|${sortByProperty ?? ""}|${currentSort ?? ""}|${searchString ?? ""}`;
|
|
1348
1386
|
let countPromise = inflightCountRequests.get(cacheKey);
|
|
1349
1387
|
if (!countPromise) {
|
|
1350
1388
|
countPromise = accessor.count({
|
|
1351
1389
|
where: whereParams,
|
|
1352
|
-
orderBy: orderByParams
|
|
1390
|
+
orderBy: orderByParams,
|
|
1391
|
+
searchString
|
|
1353
1392
|
});
|
|
1354
1393
|
inflightCountRequests.set(cacheKey, countPromise);
|
|
1355
1394
|
// Clean up the inflight entry once resolved/rejected
|
|
@@ -1364,7 +1403,7 @@ function EntitiesCount({
|
|
|
1364
1403
|
});
|
|
1365
1404
|
|
|
1366
1405
|
return () => { cancelled = true; };
|
|
1367
|
-
}, [path, filterKey, sortByProperty, currentSort]);
|
|
1406
|
+
}, [path, filterKey, sortByProperty, currentSort, searchString]);
|
|
1368
1407
|
|
|
1369
1408
|
// Count is now displayed in the breadcrumb bar, this component only fetches and reports
|
|
1370
1409
|
return null;
|
|
@@ -104,6 +104,13 @@ parentEntityIds,
|
|
|
104
104
|
</Tooltip>
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
+
// Whether the toolbar is showing icon-only controls. The filters button
|
|
108
|
+
// drops its label — and its icon a size with it — on a narrow layout as
|
|
109
|
+
// well as in the split view, and the sort and clear controls beside it
|
|
110
|
+
// follow: keyed on `compact` alone they stayed 20px next to a 16px filter
|
|
111
|
+
// icon at every width below `largeLayout`.
|
|
112
|
+
const iconOnlyToolbar = !largeLayout || Boolean(compact);
|
|
113
|
+
|
|
107
114
|
const filtersButton = resolvedProperties && tableController.setFilterValues && (
|
|
108
115
|
<Tooltip title={t("filters")}
|
|
109
116
|
key={"filters_tooltip"}>
|
|
@@ -147,6 +154,7 @@ parentEntityIds,
|
|
|
147
154
|
key={"sort_button"}
|
|
148
155
|
tableController={tableController}
|
|
149
156
|
properties={resolvedProperties}
|
|
157
|
+
compact={iconOnlyToolbar}
|
|
150
158
|
/>
|
|
151
159
|
);
|
|
152
160
|
|
|
@@ -170,6 +178,7 @@ parentEntityIds,
|
|
|
170
178
|
<ClearFilterSortButton
|
|
171
179
|
key={"clear_filter"}
|
|
172
180
|
tableController={tableController}
|
|
181
|
+
compact={iconOnlyToolbar}
|
|
173
182
|
enabled={!collection.fixedFilter}/>,
|
|
174
183
|
filterPresetsButton
|
|
175
184
|
];
|
|
@@ -16,9 +16,14 @@ import {
|
|
|
16
16
|
} from "@rebasepro/app";
|
|
17
17
|
import { useAnalyticsController } from "@rebasepro/app";
|
|
18
18
|
import { IconForView } from "@rebasepro/app";
|
|
19
|
-
import { useCollectionSlotKeys,
|
|
19
|
+
import { useCollectionSlotKeys, useEntitySlots } from "./usePreviewSlots";
|
|
20
|
+
import { Highlighted } from "./SearchHighlight";
|
|
21
|
+
import { useSearchExplanation, MatchExplanation, fieldLabel } from "./SearchExplanation";
|
|
22
|
+
import { SlotValue, TagChips } from "./SlotValue";
|
|
20
23
|
|
|
21
24
|
export type EntityCardBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
25
|
+
/** The active search, so a card can mark the hit and say where it was. */
|
|
26
|
+
searchString?: string;
|
|
22
27
|
entity: Entity<M>;
|
|
23
28
|
collection: AdminCollection<M>;
|
|
24
29
|
onClick?: (entity: Entity<M>) => void;
|
|
@@ -44,7 +49,8 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
44
49
|
highlighted,
|
|
45
50
|
onSelectionChange,
|
|
46
51
|
selectionEnabled,
|
|
47
|
-
size = "m"
|
|
52
|
+
size = "m",
|
|
53
|
+
searchString
|
|
48
54
|
}: EntityCardBindingProps<M>) {
|
|
49
55
|
const authController = useAuthController();
|
|
50
56
|
const analyticsController = useAnalyticsController();
|
|
@@ -56,13 +62,19 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
56
62
|
customizationController.propertyConfigs
|
|
57
63
|
);
|
|
58
64
|
|
|
59
|
-
const slots =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const slots = useEntitySlots(
|
|
66
|
+
entity as Entity<Record<string, unknown>>,
|
|
67
|
+
collection as AdminCollection<Record<string, unknown>>,
|
|
68
|
+
slotKeys
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Same explanation the list shows, so a reader switching view modes does
|
|
72
|
+
// not lose the reason a card is in front of them.
|
|
73
|
+
const { terms, offSlot } = useSearchExplanation(
|
|
74
|
+
entity,
|
|
75
|
+
collection.properties as Record<string, unknown>,
|
|
76
|
+
[slotKeys.titleKey, slotKeys.subtitleKey],
|
|
77
|
+
searchString
|
|
66
78
|
);
|
|
67
79
|
|
|
68
80
|
const handleClick = (e?: React.MouseEvent) => {
|
|
@@ -104,13 +116,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
104
116
|
<div className="aspect-[4/3] relative overflow-hidden bg-surface-100 dark:bg-surface-900">
|
|
105
117
|
{slots.image ? (
|
|
106
118
|
<div className="w-full h-full">
|
|
107
|
-
<
|
|
108
|
-
property={slots.image.property}
|
|
109
|
-
propertyKey={slots.image.propertyKey}
|
|
110
|
-
size="medium"
|
|
111
|
-
value={slots.image.value}
|
|
112
|
-
fill={true}
|
|
113
|
-
/>
|
|
119
|
+
<SlotValue slot={slots.image} size="medium" fill={true}/>
|
|
114
120
|
</div>
|
|
115
121
|
) : (
|
|
116
122
|
<div className="w-full h-full flex items-center justify-center">
|
|
@@ -160,12 +166,9 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
160
166
|
{/* Title slot */}
|
|
161
167
|
<div className="truncate my-1 text-sm font-medium min-h-[20px]">
|
|
162
168
|
{slots.title ? (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
property={slots.title.property}
|
|
167
|
-
size="small"
|
|
168
|
-
/>
|
|
169
|
+
terms.length > 0 && typeof slots.title.value === "string"
|
|
170
|
+
? <Highlighted text={slots.title.value} terms={terms}/>
|
|
171
|
+
: <SlotValue slot={slots.title} size="small"/>
|
|
169
172
|
) : (
|
|
170
173
|
<Typography variant="body2" className="text-surface-500">
|
|
171
174
|
{entity.id}
|
|
@@ -173,15 +176,19 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
173
176
|
)}
|
|
174
177
|
</div>
|
|
175
178
|
|
|
176
|
-
{/* Subtitle slot
|
|
177
|
-
|
|
179
|
+
{/* Subtitle slot — or, while searching, where the hit was. A
|
|
180
|
+
card whose match is in a field it does not show looks as
|
|
181
|
+
arbitrary as a list row does. */}
|
|
182
|
+
{offSlot ? (
|
|
183
|
+
<MatchExplanation
|
|
184
|
+
match={offSlot}
|
|
185
|
+
label={fieldLabel(collection.properties as Record<string, unknown>, offSlot.field)}
|
|
186
|
+
/>
|
|
187
|
+
) : slots.subtitle && (
|
|
178
188
|
<div className="line-clamp-3 [&_div]:line-clamp-3 text-xs text-surface-600 dark:text-surface-400 [&_p]:!my-1 [&_p:first-child]:!mt-0 [&_p:last-child]:!mb-0">
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
property={slots.subtitle.property}
|
|
183
|
-
size="small"
|
|
184
|
-
/>
|
|
189
|
+
{terms.length > 0 && typeof slots.subtitle.value === "string"
|
|
190
|
+
? <Highlighted text={slots.subtitle.value} terms={terms}/>
|
|
191
|
+
: <SlotValue slot={slots.subtitle} size="small"/>}
|
|
185
192
|
</div>
|
|
186
193
|
)}
|
|
187
194
|
|
|
@@ -208,15 +215,20 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
208
215
|
</div>
|
|
209
216
|
)}
|
|
210
217
|
|
|
211
|
-
{/* Status slot */}
|
|
212
|
-
{slots.status && (
|
|
213
|
-
<div className="mt-1">
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
{/* Status + tags slot — "free chips beside the status" */}
|
|
219
|
+
{(slots.status || slots.tags) && (
|
|
220
|
+
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
|
221
|
+
{slots.status && <SlotValue slot={slots.status} size="small"/>}
|
|
222
|
+
{slots.tags && <TagChips slot={slots.tags}/>}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Date slot — a card is a list row with the image on top, and the
|
|
227
|
+
row has always shown it. It was the one slot the card resolved
|
|
228
|
+
and never rendered. */}
|
|
229
|
+
{slots.date && (
|
|
230
|
+
<div className="mt-1 text-[10px] text-surface-500 dark:text-surface-400">
|
|
231
|
+
{slots.date.formatted}
|
|
220
232
|
</div>
|
|
221
233
|
)}
|
|
222
234
|
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import type { SearchMatch } from "@rebasepro/types";
|
|
3
|
+
import { Snippet, searchTerms, offSlotMatch, localRowMatch } from "./SearchHighlight";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The one place a view asks "why is this row here?" and gets something to draw.
|
|
7
|
+
*
|
|
8
|
+
* List, cards and board all render the same title-and-subtitle slots over the
|
|
9
|
+
* same machinery. An explanation that shows up in one of them and not the others
|
|
10
|
+
* reads as randomness rather than as a feature, so the decision and the markup
|
|
11
|
+
* live here and each view calls them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Terms to mark inside values the row already shows, plus the single match
|
|
16
|
+
* worth putting where the subtitle would go — or nothing, when the row is not
|
|
17
|
+
* the product of a search or the hit is already visible.
|
|
18
|
+
*
|
|
19
|
+
* Prefers the server's `_matches`, which knows the stemming the index used, and
|
|
20
|
+
* falls back to reading the row directly. That fallback is not belt-and-braces:
|
|
21
|
+
* rows reach the panel over REST, the realtime socket and the offline cache,
|
|
22
|
+
* and only one of those carries query metadata.
|
|
23
|
+
*/
|
|
24
|
+
export function useSearchExplanation(
|
|
25
|
+
entity: { values?: Record<string, unknown>; searchMatches?: SearchMatch[] },
|
|
26
|
+
properties: Record<string, unknown> | undefined,
|
|
27
|
+
shownKeys: (string | undefined)[],
|
|
28
|
+
searchString: string | undefined
|
|
29
|
+
): { terms: string[]; offSlot: SearchMatch | undefined } {
|
|
30
|
+
const terms = useMemo(() => searchTerms(searchString), [searchString]);
|
|
31
|
+
|
|
32
|
+
// Joined so the memo compares by value: the caller builds this array inline,
|
|
33
|
+
// so its identity changes every render and would defeat the memo entirely.
|
|
34
|
+
//
|
|
35
|
+
// `|` is the separator because a property path is identifiers and dots — it
|
|
36
|
+
// cannot contain one, so the round trip is lossless. (A NUL would also be
|
|
37
|
+
// unambiguous and is what this first used; a raw control byte in a source
|
|
38
|
+
// file makes grep treat the whole file as binary, and the repo rejects it.)
|
|
39
|
+
const shownKey = shownKeys.map(k => k ?? "").join("|");
|
|
40
|
+
|
|
41
|
+
const offSlot = useMemo(
|
|
42
|
+
() => {
|
|
43
|
+
if (terms.length === 0) return undefined;
|
|
44
|
+
const shown = shownKey.split("|").map(k => k || undefined);
|
|
45
|
+
return offSlotMatch(entity.searchMatches, shown)
|
|
46
|
+
?? localRowMatch(entity.values, properties, terms, shown);
|
|
47
|
+
},
|
|
48
|
+
[entity.searchMatches, entity.values, properties, shownKey, terms]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return { terms, offSlot };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Which field the hit was in, and the text around it.
|
|
56
|
+
*
|
|
57
|
+
* Sits where the subtitle would have gone. Deliberately quiet — a small amber
|
|
58
|
+
* caption and one line — because it is context for a result, not the result. It
|
|
59
|
+
* has to survive a narrow column, so it truncates rather than wraps.
|
|
60
|
+
*/
|
|
61
|
+
export function MatchExplanation({ match, label }: { match: SearchMatch; label: string }) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="mt-0.5 flex items-baseline gap-1.5 min-w-0">
|
|
64
|
+
<span
|
|
65
|
+
className="shrink-0 text-[10px] uppercase tracking-wide font-semibold text-amber-700 dark:text-amber-500/90"
|
|
66
|
+
title={match.field}
|
|
67
|
+
>
|
|
68
|
+
{label}
|
|
69
|
+
</span>
|
|
70
|
+
<span className="truncate text-xs text-surface-500 dark:text-surface-400 min-w-0">
|
|
71
|
+
<Snippet text={match.snippet}/>
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* What to call a matched field on screen.
|
|
79
|
+
*
|
|
80
|
+
* The path is what the config and the server speak — `questionnaire.
|
|
81
|
+
* certifications` — and is unreadable in a list. Resolve it to the property's
|
|
82
|
+
* declared `name`, walking into `map` properties for a dotted path, and fall
|
|
83
|
+
* back to the last segment humanised, so an unnamed field still reads as words
|
|
84
|
+
* rather than as a path.
|
|
85
|
+
*/
|
|
86
|
+
export function fieldLabel(properties: Record<string, unknown> | undefined, path: string): string {
|
|
87
|
+
const segments = path.split(".");
|
|
88
|
+
let node = properties;
|
|
89
|
+
let named: string | undefined;
|
|
90
|
+
|
|
91
|
+
for (const segment of segments) {
|
|
92
|
+
const property = node?.[segment] as { name?: string; properties?: Record<string, unknown> } | undefined;
|
|
93
|
+
// A path can run deeper than the schema describes — `content.value`,
|
|
94
|
+
// where `content` is an array of blocks whose shape is not declared
|
|
95
|
+
// property-by-property. Keep the last name that *was* declared rather
|
|
96
|
+
// than discarding it: "Content" is the answer a reader wants, and
|
|
97
|
+
// resetting to undefined here labelled that field "Value".
|
|
98
|
+
if (!property) break;
|
|
99
|
+
named = property.name ?? named;
|
|
100
|
+
node = property.properties;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (named) return named;
|
|
104
|
+
const last = segments[segments.length - 1] ?? path;
|
|
105
|
+
return last.replace(/[_-]+/g, " ").replace(/^./, c => c.toUpperCase());
|
|
106
|
+
}
|