@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
|
@@ -79,6 +79,21 @@ function isListShortcutTarget(target: HTMLElement | null, detailPanel: HTMLEleme
|
|
|
79
79
|
return !detailPanel?.contains(target);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* The list's share of the split, as a percentage, before anyone drags it.
|
|
84
|
+
*
|
|
85
|
+
* The list only has to identify a record — a title, a subtitle and a status
|
|
86
|
+
* chip — while the detail beside it carries the whole record and its rail. At
|
|
87
|
+
* 30% the list was taking width from the side that had four columns of fields
|
|
88
|
+
* to lay out; 26% still was, by about the width of one field.
|
|
89
|
+
*
|
|
90
|
+
* A default, not a constraint: {@link savePanelSize} remembers the drag per
|
|
91
|
+
* collection, so anyone who has already sized this list keeps their size. The
|
|
92
|
+
* drag floor is `minPanelSizePx`, and it holds below ~1100px of split — at that
|
|
93
|
+
* width this percentage is already the floor.
|
|
94
|
+
*/
|
|
95
|
+
const DEFAULT_PANEL_SIZE = 22;
|
|
96
|
+
|
|
82
97
|
function getSavedPanelSize(path: string): number {
|
|
83
98
|
try {
|
|
84
99
|
const saved = localStorage.getItem(`${PANEL_SIZE_KEY}_${path}`);
|
|
@@ -87,7 +102,7 @@ function getSavedPanelSize(path: string): number {
|
|
|
87
102
|
if (!isNaN(val) && val > 0 && val < 100) return val;
|
|
88
103
|
}
|
|
89
104
|
} catch { /* ignore */ }
|
|
90
|
-
return
|
|
105
|
+
return DEFAULT_PANEL_SIZE;
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
function savePanelSize(path: string, size: number) {
|
|
@@ -150,8 +165,20 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
150
165
|
dirty.current = modified;
|
|
151
166
|
}, []);
|
|
152
167
|
|
|
153
|
-
// The form remounts per entity, so its dirty state must not leak into the
|
|
168
|
+
// The form remounts per entity, so its dirty state must not leak into the
|
|
169
|
+
// next one — and it has to be cleared here rather than left to the next
|
|
170
|
+
// form's own first report, because that form spends the load in a skeleton
|
|
171
|
+
// and a navigation during it would prompt about the record before it.
|
|
172
|
+
//
|
|
173
|
+
// On a *change* of record only, never on this view's own mount: a record
|
|
174
|
+
// arriving from full screen carries its edit back in and opens dirty, and
|
|
175
|
+
// the form says so from its first effect — which runs before this one, being
|
|
176
|
+
// a child's. Clearing unconditionally overwrote that report with `false`,
|
|
177
|
+
// and the way out of the split stopped asking about a live edit.
|
|
178
|
+
const previousEntityId = useRef(selectedEntityId);
|
|
154
179
|
useEffect(() => {
|
|
180
|
+
if (previousEntityId.current === selectedEntityId) return;
|
|
181
|
+
previousEntityId.current = selectedEntityId;
|
|
155
182
|
dirty.current = false;
|
|
156
183
|
}, [selectedEntityId]);
|
|
157
184
|
|
|
@@ -170,12 +197,19 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
170
197
|
return true;
|
|
171
198
|
}, []));
|
|
172
199
|
|
|
173
|
-
// Panel size state with persistence
|
|
200
|
+
// Panel size state with persistence.
|
|
201
|
+
//
|
|
202
|
+
// Written on drag, not on mount. An effect that saved on mount stored
|
|
203
|
+
// {@link DEFAULT_PANEL_SIZE} under this collection's key the first time
|
|
204
|
+
// anyone opened the split — after which the default was frozen in every
|
|
205
|
+
// browser that had ever seen it, and changing it here reached nobody but a
|
|
206
|
+
// new visitor. Only a size someone chose is a size worth remembering.
|
|
174
207
|
const [panelSize, setPanelSize] = useState(() => getSavedPanelSize(path));
|
|
175
208
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
209
|
+
const handlePanelSizeChange = useCallback((size: number) => {
|
|
210
|
+
setPanelSize(size);
|
|
211
|
+
savePanelSize(path, size);
|
|
212
|
+
}, [path]);
|
|
179
213
|
|
|
180
214
|
// ── Animation state ──
|
|
181
215
|
// We track the "rendered" entity to keep the detail panel mounted during the exit animation.
|
|
@@ -366,6 +400,7 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
366
400
|
parentEntityIds={usedParentEntityIds}
|
|
367
401
|
selectedTab={selectedTab}
|
|
368
402
|
layout="split"
|
|
403
|
+
onCloseRequest={handleCloseDetail}
|
|
369
404
|
onEditClick={() => {
|
|
370
405
|
navigate(buildUrl(`${path}/${renderedEntityId}/edit`));
|
|
371
406
|
}}
|
|
@@ -387,6 +422,11 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
387
422
|
parentEntityIds={usedParentEntityIds}
|
|
388
423
|
selectedTab={selectedTab}
|
|
389
424
|
layout="split"
|
|
425
|
+
// The ✕, and where "save and close" lands. A plain
|
|
426
|
+
// navigation: `useNavigationBlocker` above already
|
|
427
|
+
// stands between it and an unsaved edit, so the ✕ asks
|
|
428
|
+
// exactly what Escape and the browser's Back ask.
|
|
429
|
+
onCloseRequest={handleCloseDetail}
|
|
390
430
|
onValuesModified={setDirty}
|
|
391
431
|
onSaved={() => {
|
|
392
432
|
setDirty(false);
|
|
@@ -431,7 +471,7 @@ export function SplitListView<M extends Record<string, unknown> = Record<string,
|
|
|
431
471
|
secondPanel={detailPanel}
|
|
432
472
|
showSecondPanel={isDetailVisible}
|
|
433
473
|
panelSizePercent={animationPhase === "entering" ? 100 : panelSize}
|
|
434
|
-
onPanelSizeChange={
|
|
474
|
+
onPanelSizeChange={handlePanelSizeChange}
|
|
435
475
|
minPanelSizePx={240}
|
|
436
476
|
/>
|
|
437
477
|
|
|
@@ -79,12 +79,17 @@ export function ViewModeToggle({
|
|
|
79
79
|
const { t } = useTranslation();
|
|
80
80
|
|
|
81
81
|
|
|
82
|
-
// Get icon for current view mode
|
|
82
|
+
// Get icon for current view mode.
|
|
83
|
+
//
|
|
84
|
+
// `small` on the trigger, matching the filters button beside it — the two
|
|
85
|
+
// labelled buttons open the toolbar together, and at 16 against its 20 this
|
|
86
|
+
// one read as the smaller of the pair. The options inside the popover stay
|
|
87
|
+
// at `smallest`, which is the size every other menu icon uses.
|
|
83
88
|
const getViewModeIcon = () => {
|
|
84
|
-
if (viewMode === "kanban") return <KanbanIcon size={iconSize.
|
|
85
|
-
if (viewMode === "cards") return <LayoutGridIcon size={iconSize.
|
|
86
|
-
if (viewMode === "table") return <TableIcon size={iconSize.
|
|
87
|
-
return <ListIcon size={iconSize.
|
|
89
|
+
if (viewMode === "kanban") return <KanbanIcon size={iconSize.small}/>;
|
|
90
|
+
if (viewMode === "cards") return <LayoutGridIcon size={iconSize.small}/>;
|
|
91
|
+
if (viewMode === "table") return <TableIcon size={iconSize.small}/>;
|
|
92
|
+
return <ListIcon size={iconSize.small}/>;
|
|
88
93
|
};
|
|
89
94
|
|
|
90
95
|
const getViewModeName = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { Entity, FilterValues } from "@rebasepro/types";
|
|
2
|
+
import { Entity, FilterValues, MAX_LIST_LIMIT } from "@rebasepro/types";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { getRelationIncludeParams, useData, useRebaseContext } from "@rebasepro/app";
|
|
5
5
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
@@ -220,6 +220,14 @@ export function useBoardDataController<M extends Record<string, unknown> = any,
|
|
|
220
220
|
// Skip if we're in the middle of cleanup
|
|
221
221
|
if (isCleaningUpRef.current) return;
|
|
222
222
|
|
|
223
|
+
// `itemCount` grows by one page per "load more" click with nothing
|
|
224
|
+
// stopping it, and a read above `MAX_LIST_LIMIT` is refused rather than
|
|
225
|
+
// trimmed — a column would stop loading with an error instead of
|
|
226
|
+
// stopping at the ceiling. Ask for what the platform serves; `hasMore`
|
|
227
|
+
// below still compares against the count the board asked for, so the
|
|
228
|
+
// column reports itself exhausted exactly where it did before.
|
|
229
|
+
const requestedCount = Math.min(itemCount, MAX_LIST_LIMIT);
|
|
230
|
+
|
|
223
231
|
const currentDataClient = dataClientRef.current;
|
|
224
232
|
const currentCollection = collectionRef.current;
|
|
225
233
|
const currentContext = contextRef.current;
|
|
@@ -406,9 +414,10 @@ values: { ...e.values,
|
|
|
406
414
|
|
|
407
415
|
const fetchOnce = () => accessor.find({
|
|
408
416
|
where: whereFilter,
|
|
409
|
-
limit:
|
|
417
|
+
limit: requestedCount,
|
|
410
418
|
orderBy: orderByParam,
|
|
411
|
-
include: includeParams
|
|
419
|
+
include: includeParams,
|
|
420
|
+
searchString: currentSearchString
|
|
412
421
|
});
|
|
413
422
|
|
|
414
423
|
const onError = (error: Error) => {
|
|
@@ -444,9 +453,14 @@ values: { ...e.values,
|
|
|
444
453
|
let liveDataReceived = false;
|
|
445
454
|
const unsubscribe = accessor.listen({
|
|
446
455
|
where: whereFilter,
|
|
447
|
-
limit:
|
|
456
|
+
limit: requestedCount,
|
|
448
457
|
orderBy: orderByParam,
|
|
449
|
-
include: includeParams
|
|
458
|
+
include: includeParams,
|
|
459
|
+
// Read into a local at the top of this function and then used
|
|
460
|
+
// nowhere, in both places it was read. The board took a search
|
|
461
|
+
// term, re-subscribed every column when it changed, and
|
|
462
|
+
// returned the same rows.
|
|
463
|
+
searchString: currentSearchString
|
|
450
464
|
}, res => {
|
|
451
465
|
liveDataReceived = true;
|
|
452
466
|
onUpdate(res.data as Entity<M>[]);
|
|
@@ -539,7 +553,10 @@ values: { ...e.values,
|
|
|
539
553
|
};
|
|
540
554
|
|
|
541
555
|
accessor.count({
|
|
542
|
-
where: whereFilter
|
|
556
|
+
where: whereFilter,
|
|
557
|
+
// Or the column header counts the unsearched collection
|
|
558
|
+
// while the column beneath it shows searched rows.
|
|
559
|
+
searchString: currentSearchString
|
|
543
560
|
}).then(count => {
|
|
544
561
|
if (isCleaningUpRef.current) return;
|
|
545
562
|
setColumnData(prev => ({
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { Property, RelationProperty } from "@rebasepro/types";
|
|
3
3
|
import type { Entity, EntityRelation } from "@rebasepro/types";
|
|
4
|
-
import type { PropertyConfig, AdminCollection } from "@rebasepro/admin-types";
|
|
4
|
+
import type { PropertyConfig, AdminCollection, EntityDisplayRole } from "@rebasepro/admin-types";
|
|
5
5
|
import { getEntityImagePreviewPropertyKey } from "@rebasepro/app";
|
|
6
|
+
import { getDisplayPropertyKey } from "@rebasepro/app";
|
|
6
7
|
import { getLeadingRelationTitleKey, getTitlePropertyCandidates, looksLikeIdentifierValue } from "@rebasepro/app";
|
|
7
8
|
import { getEntityFromCache } from "@rebasepro/app";
|
|
8
9
|
import { getEntityPreviewKeys } from "../../util/previews";
|
|
9
|
-
import { getValueInPath } from "@rebasepro/utils";
|
|
10
|
+
import { formatRelativeTime, getValueInPath } from "@rebasepro/utils";
|
|
10
11
|
import type { AuthController } from "@rebasepro/admin-types";
|
|
11
12
|
import { ChipColorScheme, CHIP_COLORS, CHIP_SEED_KEYS } from "@rebasepro/ui";
|
|
13
|
+
import { useEntityDisplay } from "../../hooks/useEntityDisplay";
|
|
12
14
|
|
|
13
15
|
/** Whether an authored relation yields many rows. Derived from its kind. */
|
|
14
16
|
function relationCardinality(relation: { kind?: string; cardinality?: string } | undefined): "one" | "many" | undefined {
|
|
@@ -23,13 +25,29 @@ function relationCardinality(relation: { kind?: string; cardinality?: string } |
|
|
|
23
25
|
// ── Slot types ────────────────────────────────────────────────────────
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* A resolved "slot"
|
|
27
|
-
*
|
|
28
|
+
* A resolved "slot": what fills one display role for one record.
|
|
29
|
+
*
|
|
30
|
+
* `property` and `propertyKey` are set when the value was read off the record,
|
|
31
|
+
* and absent when a `display` resolver computed it — a computed title has no
|
|
32
|
+
* column behind it, so there is nothing to render it *as*. Renderers must go
|
|
33
|
+
* through {@link SlotValue}, which makes that choice once: a property renders
|
|
34
|
+
* with its own preview (an enum keeps its coloured chip, a date its format), a
|
|
35
|
+
* computed value renders as text.
|
|
28
36
|
*/
|
|
29
37
|
export interface PreviewSlot {
|
|
30
|
-
property
|
|
31
|
-
propertyKey
|
|
38
|
+
property?: Property;
|
|
39
|
+
propertyKey?: string;
|
|
32
40
|
value: unknown;
|
|
41
|
+
/**
|
|
42
|
+
* Which display role this slot fills.
|
|
43
|
+
*
|
|
44
|
+
* Load-bearing for the computed case. Without a property there is nothing to
|
|
45
|
+
* infer from, and "render it as text" is right for a title and wrong for an
|
|
46
|
+
* image: a resolver that returned a URL had that URL printed inside the card's
|
|
47
|
+
* image frame. The role is the one thing that distinguishes them, so it
|
|
48
|
+
* travels with the slot rather than being re-derived at each call site.
|
|
49
|
+
*/
|
|
50
|
+
role?: EntityDisplayRole;
|
|
33
51
|
}
|
|
34
52
|
|
|
35
53
|
/**
|
|
@@ -89,6 +107,19 @@ export interface EntityPreviewSlots {
|
|
|
89
107
|
status: PreviewSlot | undefined;
|
|
90
108
|
/** META — date / timestamp */
|
|
91
109
|
date: DatePreviewSlot | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* TAGS — free chips beside the status.
|
|
112
|
+
*
|
|
113
|
+
* Declared only. Every other role has an inference ladder behind it, and
|
|
114
|
+
* this one deliberately does not: there is no "first enum" or "looks like a
|
|
115
|
+
* timestamp" that means *labels*, so guessing would scatter arbitrary chips
|
|
116
|
+
* across every row of every panel that never asked for them.
|
|
117
|
+
*
|
|
118
|
+
* The role existed on `EntityDisplay` and in `useEntityDisplay` before this
|
|
119
|
+
* field did, which is why a declared `display.tags` resolved correctly and
|
|
120
|
+
* then had nowhere to go.
|
|
121
|
+
*/
|
|
122
|
+
tags: PreviewSlot | undefined;
|
|
92
123
|
/** Entity ID (always available) */
|
|
93
124
|
entityId: string | number;
|
|
94
125
|
}
|
|
@@ -108,6 +139,8 @@ export interface CollectionSlotKeys {
|
|
|
108
139
|
relationKeys: string[];
|
|
109
140
|
statusKey: string | undefined;
|
|
110
141
|
dateKey: string | undefined;
|
|
142
|
+
/** Declared only — see {@link EntityPreviewSlots.tags}. */
|
|
143
|
+
tagsKey: string | undefined;
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
/**
|
|
@@ -129,10 +162,19 @@ export function resolveCollectionSlotKeys(
|
|
|
129
162
|
? [leadingRelationKey, ...rankedCandidates.filter(key => key !== leadingRelationKey)]
|
|
130
163
|
: rankedCandidates;
|
|
131
164
|
const titleKey = titleKeyCandidates[0];
|
|
165
|
+
// `getEntityImagePreviewPropertyKey` prefers `display.image` itself, so
|
|
166
|
+
// every caller of it — this, the reference card, the board — agrees.
|
|
132
167
|
const imageKey = getEntityImagePreviewPropertyKey(collection);
|
|
133
168
|
|
|
134
|
-
// Status: first string-enum that isn't the title
|
|
135
|
-
|
|
169
|
+
// Status: declared, else the first string-enum that isn't the title.
|
|
170
|
+
//
|
|
171
|
+
// Everything below this line is inference — "the first enum", "a key that
|
|
172
|
+
// looks like a timestamp", "the longest remaining string". It is a good
|
|
173
|
+
// guess and it is only a guess, so a collection that states the role wins
|
|
174
|
+
// outright and the ladder is skipped. That is the whole point of
|
|
175
|
+
// `admin.display`: the heuristics stay for the collections that never say
|
|
176
|
+
// anything, and stop overruling the ones that do.
|
|
177
|
+
let statusKey: string | undefined = getDisplayPropertyKey(collection, "status");
|
|
136
178
|
|
|
137
179
|
// 1. Explicitly defined in previewProperties
|
|
138
180
|
if (!statusKey && collection.previewProperties) {
|
|
@@ -169,13 +211,15 @@ export function resolveCollectionSlotKeys(
|
|
|
169
211
|
}
|
|
170
212
|
}
|
|
171
213
|
|
|
172
|
-
// Date:
|
|
173
|
-
let dateKey: string | undefined;
|
|
214
|
+
// Date: declared, else a well-known timestamp field, else any date
|
|
215
|
+
let dateKey: string | undefined = getDisplayPropertyKey(collection, "date");
|
|
174
216
|
const dateCandidates = ["updated_at", "updatedAt", "modified_at", "modifiedAt", "created_at", "createdAt"];
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
217
|
+
if (!dateKey) {
|
|
218
|
+
for (const candidate of dateCandidates) {
|
|
219
|
+
if (collection.properties[candidate]) {
|
|
220
|
+
dateKey = candidate;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
179
223
|
}
|
|
180
224
|
}
|
|
181
225
|
if (!dateKey) {
|
|
@@ -222,7 +266,8 @@ export function resolveCollectionSlotKeys(
|
|
|
222
266
|
const scoreB = propB?.type === "string" ? (propB.admin?.multiline ? 2 : 1) : 0;
|
|
223
267
|
return scoreB - scoreA;
|
|
224
268
|
});
|
|
225
|
-
const subtitleKey =
|
|
269
|
+
const subtitleKey = getDisplayPropertyKey(collection, "subtitle")
|
|
270
|
+
?? (sortedPreviewKeys.length > 0 ? sortedPreviewKeys[0] : undefined);
|
|
226
271
|
|
|
227
272
|
return { titleKey,
|
|
228
273
|
titleKeyCandidates,
|
|
@@ -230,7 +275,8 @@ imageKey,
|
|
|
230
275
|
subtitleKey,
|
|
231
276
|
relationKeys,
|
|
232
277
|
statusKey,
|
|
233
|
-
dateKey
|
|
278
|
+
dateKey,
|
|
279
|
+
tagsKey: getDisplayPropertyKey(collection, "tags") };
|
|
234
280
|
}
|
|
235
281
|
|
|
236
282
|
// ── Image unwrapping helper ───────────────────────────────────────────
|
|
@@ -261,6 +307,7 @@ function resolveImageSlot(
|
|
|
261
307
|
if (resolvedValue === undefined || resolvedValue === null) return undefined;
|
|
262
308
|
|
|
263
309
|
return {
|
|
310
|
+
role: "image",
|
|
264
311
|
property: resolvedProperty as Property,
|
|
265
312
|
propertyKey: imageKey,
|
|
266
313
|
value: resolvedValue
|
|
@@ -299,15 +346,11 @@ function formatDateValue(value: unknown): string {
|
|
|
299
346
|
if (isNaN(date.getTime())) return String(value);
|
|
300
347
|
|
|
301
348
|
const now = new Date();
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
if (diffMins < 1) return "Just now";
|
|
308
|
-
if (diffMins < 60) return `${diffMins}m ago`;
|
|
309
|
-
if (diffHours < 24) return `${diffHours}h ago`;
|
|
310
|
-
if (diffDays < 7) return `${diffDays}d ago`;
|
|
349
|
+
// A date slot holds whatever property the collection points it at, which is
|
|
350
|
+
// as often a due or publish date as a `updated_at`. `formatRelativeTime`
|
|
351
|
+
// reads the sign, so a scheduled date says "in 3d" instead of "Just now".
|
|
352
|
+
const relative = formatRelativeTime(date, { now });
|
|
353
|
+
if (relative) return relative;
|
|
311
354
|
|
|
312
355
|
return date.toLocaleDateString(undefined, {
|
|
313
356
|
month: "short",
|
|
@@ -348,7 +391,7 @@ export function resolveEntitySlots(
|
|
|
348
391
|
collection: AdminCollection<Record<string, unknown>>,
|
|
349
392
|
slotKeys: CollectionSlotKeys
|
|
350
393
|
): EntityPreviewSlots {
|
|
351
|
-
const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey } = slotKeys;
|
|
394
|
+
const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey, tagsKey } = slotKeys;
|
|
352
395
|
|
|
353
396
|
// Image
|
|
354
397
|
const image = imageKey ? resolveImageSlot(collection, imageKey, entity) : undefined;
|
|
@@ -361,7 +404,8 @@ export function resolveEntitySlots(
|
|
|
361
404
|
if (resolvedTitleKey) {
|
|
362
405
|
const prop = collection.properties[resolvedTitleKey] as Property | undefined;
|
|
363
406
|
const val = getValueInPath(entity.values, resolvedTitleKey);
|
|
364
|
-
if (prop) title = {
|
|
407
|
+
if (prop) title = { role: "title",
|
|
408
|
+
property: prop,
|
|
365
409
|
propertyKey: resolvedTitleKey,
|
|
366
410
|
value: val };
|
|
367
411
|
}
|
|
@@ -372,7 +416,8 @@ value: val };
|
|
|
372
416
|
const prop = collection.properties[subtitleKey] as Property | undefined;
|
|
373
417
|
const val = getValueInPath(entity.values, subtitleKey);
|
|
374
418
|
if (prop && val !== undefined && val !== null && val !== "") {
|
|
375
|
-
subtitle = {
|
|
419
|
+
subtitle = { role: "subtitle",
|
|
420
|
+
property: prop,
|
|
376
421
|
propertyKey: subtitleKey,
|
|
377
422
|
value: val };
|
|
378
423
|
}
|
|
@@ -448,7 +493,8 @@ id: relation.id });
|
|
|
448
493
|
const prop = collection.properties[statusKey] as Property | undefined;
|
|
449
494
|
const val = getValueInPath(entity.values, statusKey);
|
|
450
495
|
if (prop && val !== undefined && val !== null) {
|
|
451
|
-
status = {
|
|
496
|
+
status = { role: "status",
|
|
497
|
+
property: prop,
|
|
452
498
|
propertyKey: statusKey,
|
|
453
499
|
value: val };
|
|
454
500
|
}
|
|
@@ -461,6 +507,7 @@ value: val };
|
|
|
461
507
|
const val = getValueInPath(entity.values, dateKey);
|
|
462
508
|
if (prop && val !== undefined && val !== null) {
|
|
463
509
|
date = {
|
|
510
|
+
role: "date",
|
|
464
511
|
property: prop,
|
|
465
512
|
propertyKey: dateKey,
|
|
466
513
|
value: val,
|
|
@@ -469,6 +516,23 @@ value: val };
|
|
|
469
516
|
}
|
|
470
517
|
}
|
|
471
518
|
|
|
519
|
+
// Tags — declared only, and empty means absent: an empty array is "this
|
|
520
|
+
// record has no labels", which must render as nothing rather than as an
|
|
521
|
+
// empty chip row that shifts every other slot down.
|
|
522
|
+
let tags: PreviewSlot | undefined;
|
|
523
|
+
if (tagsKey) {
|
|
524
|
+
const prop = collection.properties[tagsKey] as Property | undefined;
|
|
525
|
+
const val = getValueInPath(entity.values, tagsKey);
|
|
526
|
+
const empty = val === undefined || val === null || val === ""
|
|
527
|
+
|| (Array.isArray(val) && val.length === 0);
|
|
528
|
+
if (prop && !empty) {
|
|
529
|
+
tags = { role: "tags",
|
|
530
|
+
property: prop,
|
|
531
|
+
propertyKey: tagsKey,
|
|
532
|
+
value: val };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
472
536
|
return {
|
|
473
537
|
image,
|
|
474
538
|
title,
|
|
@@ -476,6 +540,7 @@ value: val };
|
|
|
476
540
|
relations,
|
|
477
541
|
status,
|
|
478
542
|
date,
|
|
543
|
+
tags,
|
|
479
544
|
entityId: entity.id
|
|
480
545
|
};
|
|
481
546
|
}
|
|
@@ -506,43 +571,26 @@ function resolveRelationDisplayName(
|
|
|
506
571
|
}
|
|
507
572
|
|
|
508
573
|
// Helper: extract display name from entity values using the target collection
|
|
574
|
+
//
|
|
575
|
+
// This used to carry a ranking of its own — priority keys `name/title/label/
|
|
576
|
+
// displayName`, then the first visible non-id string — which is a *seventh*
|
|
577
|
+
// answer to "what is this record called", and one that disagreed with the
|
|
578
|
+
// shared one: it read `titleProperty` with a flat lookup (so a dotted path
|
|
579
|
+
// was ignored), never saw `display.title`, and ranked by key name where the
|
|
580
|
+
// shared ranking excludes identifiers structurally. A chip and the record it
|
|
581
|
+
// points at could therefore be labelled differently on the same screen.
|
|
582
|
+
//
|
|
583
|
+
// What is left here is the part that is genuinely local: a relation carries
|
|
584
|
+
// *eagerly loaded* values, not an entity, so a resolver cannot run against
|
|
585
|
+
// it — a computed title falls back to the ranked property, which is the same
|
|
586
|
+
// thing the list row shows while its own resolver is in flight.
|
|
509
587
|
const extractDisplayName = (values: Record<string, unknown>): string | undefined => {
|
|
510
588
|
if (targetCollection) {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// Helper to check if a property is hidden/internal
|
|
517
|
-
const isHiddenProp = (p: Property): boolean => {
|
|
518
|
-
if (p.admin?.hideFromCollection) return true;
|
|
519
|
-
if (typeof p.admin?.disabled === "object" && p.admin.disabled.hidden) return true;
|
|
520
|
-
return false;
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Helper to check if a property is a visible, non-id string
|
|
524
|
-
const isDisplayCandidate = (p: Property): boolean => {
|
|
525
|
-
return p.type === "string" && !p.admin?.multiline && !p.admin?.markdown && !p.storage
|
|
526
|
-
&& !("isId" in p && p.isId) && !isHiddenProp(p);
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
// Prioritize common title-like fields: name, title, label, displayName
|
|
530
|
-
const priorityKeys = ["name", "title", "label", "displayName"];
|
|
531
|
-
for (const pk of priorityKeys) {
|
|
532
|
-
const p = targetCollection.properties[pk] as Property | undefined;
|
|
533
|
-
if (p && isDisplayCandidate(p) && values[pk] !== undefined && values[pk] !== null) {
|
|
534
|
-
return String(values[pk]);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Fallback: find first visible, non-id string property in target
|
|
539
|
-
for (const [k, p] of Object.entries(targetCollection.properties)) {
|
|
540
|
-
const tp = p as Property;
|
|
541
|
-
if (isDisplayCandidate(tp)) {
|
|
542
|
-
if (values[k] !== undefined && values[k] !== null) {
|
|
543
|
-
return String(values[k]);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
589
|
+
for (const key of getTitlePropertyCandidates(targetCollection)) {
|
|
590
|
+
const value = getValueInPath(values, key);
|
|
591
|
+
if (value === undefined || value === null || value === "") continue;
|
|
592
|
+
if (typeof value === "string" && looksLikeIdentifierValue(value)) continue;
|
|
593
|
+
return String(value);
|
|
546
594
|
}
|
|
547
595
|
}
|
|
548
596
|
// Generic fallback: walk entity values for any short string.
|
|
@@ -582,3 +630,74 @@ function resolveRelationDisplayName(
|
|
|
582
630
|
// 3. Final fallback: show the ID
|
|
583
631
|
return id !== undefined ? String(id) : undefined;
|
|
584
632
|
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* The slots for one record, with `admin.display` resolvers applied.
|
|
636
|
+
*
|
|
637
|
+
* {@link resolveEntitySlots} answers the question from values already in hand —
|
|
638
|
+
* which is everything a slot filled by a property needs, and nothing a slot
|
|
639
|
+
* filled by a *resolver* can use. A computed title may have to be fetched, so it
|
|
640
|
+
* arrives after the first render; this hook is where the two meet.
|
|
641
|
+
*
|
|
642
|
+
* Until a resolver lands, the derived slot shows. That is deliberate: a list of
|
|
643
|
+
* fifty rows must not blank its titles for a beat while fifty promises settle,
|
|
644
|
+
* and the derived answer — the best-ranked property carrying a readable value —
|
|
645
|
+
* is a reasonable thing to be looking at meanwhile. When the resolved value
|
|
646
|
+
* arrives it replaces the slot outright, property and all, because there is no
|
|
647
|
+
* property behind a computed value (see {@link PreviewSlot}).
|
|
648
|
+
*
|
|
649
|
+
* Every role is hooked unconditionally, so the hook order is fixed regardless of
|
|
650
|
+
* what a given collection declares.
|
|
651
|
+
*/
|
|
652
|
+
export function useEntitySlots(
|
|
653
|
+
entity: Entity<Record<string, unknown>>,
|
|
654
|
+
collection: AdminCollection<Record<string, unknown>>,
|
|
655
|
+
slotKeys: CollectionSlotKeys
|
|
656
|
+
): EntityPreviewSlots {
|
|
657
|
+
|
|
658
|
+
const derived = useMemo(
|
|
659
|
+
() => resolveEntitySlots(entity, collection, slotKeys),
|
|
660
|
+
[entity, collection, slotKeys]
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const params = { collection,
|
|
664
|
+
entity };
|
|
665
|
+
const title = useEntityDisplay<string>("title", params);
|
|
666
|
+
const subtitle = useEntityDisplay<string>("subtitle", params);
|
|
667
|
+
const image = useEntityDisplay<string>("image", params);
|
|
668
|
+
const status = useEntityDisplay<string>("status", params);
|
|
669
|
+
const date = useEntityDisplay<Date | string | number>("date", params);
|
|
670
|
+
const tags = useEntityDisplay<string[] | string>("tags", params);
|
|
671
|
+
|
|
672
|
+
return useMemo(() => {
|
|
673
|
+
// `source` is set only when the value came from a property, so its
|
|
674
|
+
// absence is exactly the "this was computed" signal — no second flag to
|
|
675
|
+
// keep in step with the first.
|
|
676
|
+
// The role travels with the computed slot too. It is the only thing left
|
|
677
|
+
// to say about a value with no property behind it, and `SlotValue` needs
|
|
678
|
+
// it to tell a computed image from a computed title.
|
|
679
|
+
const computed = (
|
|
680
|
+
role: EntityDisplayRole,
|
|
681
|
+
resolved: { value: unknown, source?: unknown }
|
|
682
|
+
): PreviewSlot | undefined =>
|
|
683
|
+
resolved.value !== undefined && resolved.source === undefined
|
|
684
|
+
? { role,
|
|
685
|
+
value: resolved.value }
|
|
686
|
+
: undefined;
|
|
687
|
+
|
|
688
|
+
const computedDate = computed("date", date);
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
...derived,
|
|
692
|
+
title: computed("title", title) ?? derived.title,
|
|
693
|
+
subtitle: computed("subtitle", subtitle) ?? derived.subtitle,
|
|
694
|
+
image: computed("image", image) ?? derived.image,
|
|
695
|
+
status: computed("status", status) ?? derived.status,
|
|
696
|
+
date: computedDate
|
|
697
|
+
? { ...computedDate,
|
|
698
|
+
formatted: formatDateValue(computedDate.value) }
|
|
699
|
+
: derived.date,
|
|
700
|
+
tags: computed("tags", tags) ?? derived.tags
|
|
701
|
+
};
|
|
702
|
+
}, [derived, title, subtitle, image, status, date, tags]);
|
|
703
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isArrayValue, readStoredJson, writeStoredJson } from "@rebasepro/utils";
|
|
2
|
+
|
|
1
3
|
export function addRecentId(collectionId: string, id: string | number) {
|
|
2
4
|
const recentIds = getRecentIds(collectionId);
|
|
3
5
|
const newRecentIds = [id, ...recentIds.filter(i => i !== id)];
|
|
@@ -9,11 +11,12 @@ export function addRecentId(collectionId: string, id: string | number) {
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function saveSearchedIdsLocally(collectionId: string, ids: (string | number)[]) {
|
|
12
|
-
|
|
14
|
+
writeStoredJson("recent_id_searches::" + collectionId, ids);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function getRecentIds(collectionId: string): (string | number)[] {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
return readStoredJson<(string | number)[]>(
|
|
19
|
+
"recent_id_searches::" + collectionId,
|
|
20
|
+
{ fallback: [], accept: isArrayValue }
|
|
21
|
+
);
|
|
19
22
|
}
|