@rebasepro/admin 0.13.0 → 0.13.1-canary.g394d868
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-D78xsXgL.js} +3 -3
- package/dist/{CollectionEditorDialog-CjXkWZ64.js.map → CollectionEditorDialog-D78xsXgL.js.map} +1 -1
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-B88EC-1f.js} +2 -2
- package/dist/{PropertyEditView-W8I3YV89.js.map → PropertyEditView-B88EC-1f.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-B1Ws7h9l.js} +4 -4
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js.map → RouterCollectionsStudioView-B1Ws7h9l.js.map} +1 -1
- 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-BZhdWhkB.js} +2 -2
- package/dist/{export-J79rMoNj.js.map → export-BZhdWhkB.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-BPpt68CL.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-BPpt68CL.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-gRfemegK.js} +3 -3
- package/dist/{import-BvYl--U4.js.map → import-gRfemegK.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-BHq3VOjH.js} +9080 -7861
- package/dist/util-BHq3VOjH.js.map +1 -0
- package/package.json +9 -9
- package/src/collection_editor/serializable_types.ts +1 -0
- 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 +172 -112
- 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 +177 -54
- package/src/components/DefaultDrawer.tsx +19 -23
- package/src/components/DetailViewBinding.tsx +87 -55
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +44 -16
- package/src/components/EditViewBinding.tsx +211 -41
- package/src/components/EntityDisplayHeader.tsx +170 -0
- package/src/components/EntityIdentityBar.tsx +149 -23
- 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/index.ts +3 -0
- 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/util-GiwPgxnL.js.map +0 -1
|
@@ -24,7 +24,8 @@ import { getEntityPreviewKeys } from "../../util/previews";
|
|
|
24
24
|
import { IconForView } from "@rebasepro/app";
|
|
25
25
|
import { getIcon } from "@rebasepro/app";
|
|
26
26
|
import { getValueInPath } from "@rebasepro/utils";
|
|
27
|
-
import { useCollectionSlotKeys,
|
|
27
|
+
import { useCollectionSlotKeys, useEntitySlots, type CollectionSlotKeys } from "./usePreviewSlots";
|
|
28
|
+
import { SlotValue, TagChips } from "./SlotValue";
|
|
28
29
|
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
29
30
|
import { resolveEntityAction } from "../../util/resolutions";
|
|
30
31
|
|
|
@@ -78,8 +79,30 @@ type ListColumnDef = {
|
|
|
78
79
|
isDate?: boolean;
|
|
79
80
|
align: "left" | "center" | "right";
|
|
80
81
|
width: string;
|
|
82
|
+
/**
|
|
83
|
+
* What {@link width} costs in pixels, so the fit calculation can budget in
|
|
84
|
+
* the same units the row lays out in. A flat per-column estimate treated a
|
|
85
|
+
* relation (w-64) as costing the same as a date (w-28), which is where a row
|
|
86
|
+
* ran out of width for the title while still claiming everything fit.
|
|
87
|
+
*/
|
|
88
|
+
widthPx: number;
|
|
81
89
|
};
|
|
82
90
|
|
|
91
|
+
/**
|
|
92
|
+
* How wide a column of this property wants to be — the Tailwind class the cell
|
|
93
|
+
* gets, and the pixel cost of that class. The two are returned together because
|
|
94
|
+
* they must not drift: the budget is only meaningful if it describes the widths
|
|
95
|
+
* actually rendered. `lg:` variants key off the viewport, not this panel, so a
|
|
96
|
+
* narrow split on a wide screen gets the larger of the two — the cost we budget.
|
|
97
|
+
*/
|
|
98
|
+
function getIdealColumnWidth(prop: Property): { width: string, widthPx: number } {
|
|
99
|
+
if (prop.type === "string" && "enum" in prop) return { width: "flex-shrink-0 w-32", widthPx: 128 };
|
|
100
|
+
if (prop.type === "date" || prop.type === "number" || prop.type === "boolean") return { width: "flex-shrink-0 w-28", widthPx: 112 };
|
|
101
|
+
if (prop.type === "reference" || prop.type === "relation") return { width: "flex-shrink-0 w-56 lg:w-64", widthPx: 256 };
|
|
102
|
+
if (prop.type === "string") return { width: "flex-shrink-0 w-40 lg:w-48", widthPx: 192 };
|
|
103
|
+
return { width: "flex-shrink-0 w-40", widthPx: 160 };
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
/**
|
|
84
107
|
* Get row padding/spacing classes based on size
|
|
85
108
|
*/
|
|
@@ -108,6 +131,30 @@ function getEstimatedRowHeight(size: CollectionSize): number {
|
|
|
108
131
|
}
|
|
109
132
|
}
|
|
110
133
|
|
|
134
|
+
// ── Row layout budget ─────────────────────────────────────────────────
|
|
135
|
+
// What the row spends before a single column is placed. These mirror the
|
|
136
|
+
// classes on the row itself (`px-5`, `gap-4`, `w-8` checkbox, `w-10` image);
|
|
137
|
+
// they are the same layout described twice, so a change to one is a change to
|
|
138
|
+
// the other.
|
|
139
|
+
|
|
140
|
+
/** `px-5` on both sides. */
|
|
141
|
+
const ROW_PADDING_WIDTH = 40;
|
|
142
|
+
/** The checkbox cell (`w-8`). */
|
|
143
|
+
const CHECKBOX_WIDTH = 32;
|
|
144
|
+
/** The thumbnail / icon square (`w-10`). */
|
|
145
|
+
const IMAGE_WIDTH = 40;
|
|
146
|
+
/** `gap-4` between every cell of the row. */
|
|
147
|
+
const COLUMN_GAP = 16;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The title's share of a row, taken before any column bids for width.
|
|
151
|
+
*
|
|
152
|
+
* A title is a name, and a name truncated to "Lider Sr. en…" has stopped being
|
|
153
|
+
* one. Columns are a scanner's convenience; the title is the row's identity, so
|
|
154
|
+
* a narrow list spends its width on the title and simply shows fewer columns.
|
|
155
|
+
*/
|
|
156
|
+
const TITLE_COMFORTABLE_WIDTH = 320;
|
|
157
|
+
|
|
111
158
|
/** Number of extra rows rendered above/below the viewport. */
|
|
112
159
|
const OVERSCAN_COUNT = 8;
|
|
113
160
|
|
|
@@ -293,14 +340,21 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
293
340
|
const context = useAdminContext();
|
|
294
341
|
|
|
295
342
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
296
|
-
|
|
343
|
+
// `undefined` is "not measured yet", and it renders the title alone. The
|
|
344
|
+
// seed used to be a 1200px guess, which is a claim about a list that may be
|
|
345
|
+
// a 300px column of a split — and, because the ref below was never attached
|
|
346
|
+
// to anything, a guess nothing ever corrected.
|
|
347
|
+
const [containerWidth, setContainerWidth] = useState<number | undefined>(undefined);
|
|
297
348
|
|
|
298
349
|
// Track container width for responsive column visibility
|
|
299
350
|
useEffect(() => {
|
|
300
351
|
const el = containerRef.current;
|
|
301
352
|
if (!el) return;
|
|
353
|
+
const measure = (width: number) => setContainerWidth(width > 0 ? width : undefined);
|
|
354
|
+
measure(el.getBoundingClientRect().width);
|
|
355
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
302
356
|
const ro = new ResizeObserver(([entry]) => {
|
|
303
|
-
if (entry)
|
|
357
|
+
if (entry) measure(entry.contentRect.width);
|
|
304
358
|
});
|
|
305
359
|
ro.observe(el);
|
|
306
360
|
return () => ro.disconnect();
|
|
@@ -343,14 +397,6 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
343
397
|
const columns = useMemo(() => {
|
|
344
398
|
const cols: ListColumnDef[] = [];
|
|
345
399
|
|
|
346
|
-
const getIdealColumnWidth = (prop: Property) => {
|
|
347
|
-
if (prop.type === "string" && "enum" in prop) return "flex-shrink-0 w-32";
|
|
348
|
-
if (prop.type === "date" || prop.type === "number" || prop.type === "boolean") return "flex-shrink-0 w-28";
|
|
349
|
-
if (prop.type === "reference" || prop.type === "relation") return "flex-shrink-0 w-56 lg:w-64";
|
|
350
|
-
if (prop.type === "string") return "flex-shrink-0 w-40 lg:w-48";
|
|
351
|
-
return "flex-shrink-0 w-40";
|
|
352
|
-
};
|
|
353
|
-
|
|
354
400
|
if (resolvedCollection.listProperties && resolvedCollection.listProperties.length > 0) {
|
|
355
401
|
resolvedCollection.listProperties.forEach((key, index) => {
|
|
356
402
|
const prop = resolvedCollection.properties[key] as Property;
|
|
@@ -362,7 +408,9 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
362
408
|
property: prop,
|
|
363
409
|
isTitle: index === 0,
|
|
364
410
|
align: prop.type === "number" || prop.type === "date" ? "right" : "left",
|
|
365
|
-
|
|
411
|
+
...(index === 0
|
|
412
|
+
? { width: "flex-1 min-w-0", widthPx: TITLE_COMFORTABLE_WIDTH }
|
|
413
|
+
: getIdealColumnWidth(prop))
|
|
366
414
|
});
|
|
367
415
|
});
|
|
368
416
|
return cols;
|
|
@@ -378,7 +426,8 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
378
426
|
property: prop,
|
|
379
427
|
isTitle: true,
|
|
380
428
|
align: "left",
|
|
381
|
-
width: "flex-1 min-w-
|
|
429
|
+
width: "flex-1 min-w-0",
|
|
430
|
+
widthPx: TITLE_COMFORTABLE_WIDTH
|
|
382
431
|
});
|
|
383
432
|
}
|
|
384
433
|
}
|
|
@@ -421,7 +470,7 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
421
470
|
label: prop.name || key,
|
|
422
471
|
property: prop,
|
|
423
472
|
align: prop.type === "number" || prop.type === "date" ? "right" : "left",
|
|
424
|
-
|
|
473
|
+
...getIdealColumnWidth(prop)
|
|
425
474
|
});
|
|
426
475
|
});
|
|
427
476
|
|
|
@@ -434,7 +483,8 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
434
483
|
property: prop,
|
|
435
484
|
isStatus: true,
|
|
436
485
|
align: "center",
|
|
437
|
-
width: "flex-shrink-0 w-32"
|
|
486
|
+
width: "flex-shrink-0 w-32",
|
|
487
|
+
widthPx: 128
|
|
438
488
|
});
|
|
439
489
|
}
|
|
440
490
|
}
|
|
@@ -448,7 +498,8 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
448
498
|
property: prop,
|
|
449
499
|
isDate: true,
|
|
450
500
|
align: "right",
|
|
451
|
-
width: "flex-shrink-0 w-28"
|
|
501
|
+
width: "flex-shrink-0 w-28",
|
|
502
|
+
widthPx: 112
|
|
452
503
|
});
|
|
453
504
|
}
|
|
454
505
|
}
|
|
@@ -458,33 +509,39 @@ export function CollectionListViewBinding<M extends Record<string, unknown> = Re
|
|
|
458
509
|
|
|
459
510
|
const showImage = size !== "xs";
|
|
460
511
|
|
|
461
|
-
// Responsive:
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
512
|
+
// Responsive: keep only the columns the row can actually pay for.
|
|
513
|
+
//
|
|
514
|
+
// The title is what a row is *for* — it is the one cell that identifies the
|
|
515
|
+
// record — so it is not one competitor among the columns: it takes its
|
|
516
|
+
// comfortable share off the top, and the columns divide what is left. Each
|
|
517
|
+
// one is charged its own rendered width, and the first that cannot be
|
|
518
|
+
// afforded ends the row: dropping a column and keeping the narrower one
|
|
519
|
+
// after it would reorder the row as it resizes.
|
|
465
520
|
const visibleColumns = useMemo(() => {
|
|
466
521
|
if (columns.length <= 1) return columns;
|
|
467
522
|
|
|
468
523
|
const titleCol = columns.find(c => c.isTitle);
|
|
469
524
|
const extraCols = columns.filter(c => !c.isTitle);
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
let
|
|
480
|
-
|
|
481
|
-
|
|
525
|
+
const withTitle = (kept: ListColumnDef[]) => [...(titleCol ? [titleCol] : []), ...kept];
|
|
526
|
+
|
|
527
|
+
// Unmeasured: the title alone, which is the answer we would rather flash
|
|
528
|
+
// than a row of columns squeezing it down to an ellipsis.
|
|
529
|
+
if (containerWidth === undefined) return withTitle([]);
|
|
530
|
+
|
|
531
|
+
const chrome = ROW_PADDING_WIDTH
|
|
532
|
+
+ (selectionEnabled ? CHECKBOX_WIDTH + COLUMN_GAP : 0)
|
|
533
|
+
+ (showImage ? IMAGE_WIDTH + COLUMN_GAP : 0);
|
|
534
|
+
let remaining = containerWidth - chrome - TITLE_COMFORTABLE_WIDTH;
|
|
535
|
+
|
|
536
|
+
const kept: ListColumnDef[] = [];
|
|
537
|
+
for (const col of extraCols) {
|
|
538
|
+
const cost = col.widthPx + COLUMN_GAP;
|
|
539
|
+
if (cost > remaining) break;
|
|
540
|
+
remaining -= cost;
|
|
541
|
+
kept.push(col);
|
|
482
542
|
}
|
|
483
543
|
|
|
484
|
-
return
|
|
485
|
-
...(titleCol ? [titleCol] : []),
|
|
486
|
-
...extraCols.slice(0, maxExtraCols)
|
|
487
|
-
];
|
|
544
|
+
return withTitle(kept);
|
|
488
545
|
}, [columns, containerWidth, selectionEnabled, showImage]);
|
|
489
546
|
|
|
490
547
|
const handleEntityClick = useCallback((entity: Entity<M>) => {
|
|
@@ -528,54 +585,56 @@ customEntityActions });
|
|
|
528
585
|
}, [handleSelectionChange]);
|
|
529
586
|
|
|
530
587
|
return (
|
|
531
|
-
<
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
588
|
+
<div ref={containerRef} className="w-full">
|
|
589
|
+
<ListView<Entity<M>>
|
|
590
|
+
data={data}
|
|
591
|
+
dataLoading={dataLoading}
|
|
592
|
+
noMoreToLoad={noMoreToLoad}
|
|
593
|
+
dataLoadingError={dataLoadingError}
|
|
594
|
+
itemCount={itemCount}
|
|
595
|
+
setItemCount={setItemCount}
|
|
596
|
+
pageSize={pageSize}
|
|
597
|
+
paginationEnabled={paginationEnabled}
|
|
598
|
+
onItemClick={handleEntityClick}
|
|
599
|
+
selectedIds={selectedIds}
|
|
600
|
+
highlightedIds={highlightedIds}
|
|
601
|
+
selectionEnabled={selectionEnabled}
|
|
602
|
+
emptyComponent={emptyComponent}
|
|
603
|
+
size={size}
|
|
604
|
+
selectedEntityId={selectedEntityId}
|
|
605
|
+
renderRow={useCallback(({ item: entity, style, className, selected, highlighted, isLast, onClick, onSelectionChange }) => {
|
|
606
|
+
return (
|
|
607
|
+
<div
|
|
608
|
+
key={entity.id}
|
|
609
|
+
style={style}
|
|
610
|
+
className={className}
|
|
611
|
+
>
|
|
612
|
+
<ListRow
|
|
613
|
+
entity={entity}
|
|
614
|
+
collection={resolvedCollection}
|
|
615
|
+
onClick={handleEntityClick}
|
|
616
|
+
selected={selected}
|
|
617
|
+
highlighted={highlighted}
|
|
618
|
+
onSelectionChange={handleRowSelectionChange}
|
|
619
|
+
selectionEnabled={selectionEnabled}
|
|
620
|
+
columns={visibleColumns}
|
|
621
|
+
slotKeys={slotKeys}
|
|
622
|
+
rowClasses={rowClasses}
|
|
623
|
+
showImage={showImage}
|
|
624
|
+
size={size}
|
|
625
|
+
isLast={isLast}
|
|
626
|
+
isActive={selectedEntityId !== undefined && entity.id === selectedEntityId}
|
|
627
|
+
listViewActions={getListViewActions(entity)}
|
|
628
|
+
context={context}
|
|
629
|
+
path={path}
|
|
630
|
+
selectionController={selectionController}
|
|
631
|
+
openEntityMode={openEntityMode}
|
|
632
|
+
/>
|
|
633
|
+
</div>
|
|
634
|
+
);
|
|
635
|
+
}, [resolvedCollection, selectionEnabled, visibleColumns, slotKeys, rowClasses, showImage, size, selectedEntityId, getListViewActions, context, path, selectionController, openEntityMode, handleRowSelectionChange, handleEntityClick])}
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
579
638
|
);
|
|
580
639
|
}
|
|
581
640
|
|
|
@@ -628,8 +687,11 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
628
687
|
selectionController?: SelectionController<M>;
|
|
629
688
|
openEntityMode?: "side_panel" | "full_screen" | "split" | "dialog";
|
|
630
689
|
}) {
|
|
631
|
-
// ── Resolve slots
|
|
632
|
-
|
|
690
|
+
// ── Resolve slots ──
|
|
691
|
+
// A hook now, not a pure call: a `display` resolver may have to fetch what
|
|
692
|
+
// fills a slot, so the row subscribes to the result and re-renders when it
|
|
693
|
+
// lands. `ListRow` is memoized, which is what keeps that to the one row.
|
|
694
|
+
const slots = useEntitySlots(
|
|
633
695
|
entity as Entity<Record<string, unknown>>,
|
|
634
696
|
collection as AdminCollection<Record<string, unknown>>,
|
|
635
697
|
slotKeys
|
|
@@ -696,7 +758,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
696
758
|
<div className="flex-shrink-0">
|
|
697
759
|
{slots.image ? (
|
|
698
760
|
<div className={cls("w-10 h-10 rounded-lg border relative overflow-hidden bg-surface-100 dark:bg-surface-900", defaultBorderMixin)}>
|
|
699
|
-
<
|
|
761
|
+
<SlotValue slot={slots.image} size="small" fill={true}/>
|
|
700
762
|
</div>
|
|
701
763
|
) : (
|
|
702
764
|
<div className={cls("w-10 h-10 rounded-lg bg-surface-100 dark:bg-surface-900 flex items-center justify-center border", defaultBorderMixin)}>
|
|
@@ -715,13 +777,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
715
777
|
<div className="truncate">
|
|
716
778
|
{slots.title?.value !== undefined ? (
|
|
717
779
|
<Typography component="div" variant="body2" className="font-semibold text-surface-900 dark:text-surface-50 truncate transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400">
|
|
718
|
-
<
|
|
719
|
-
propertyKey={slots.title.propertyKey}
|
|
720
|
-
value={slots.title.value}
|
|
721
|
-
property={slots.title.property}
|
|
722
|
-
size="small"
|
|
723
|
-
textOnly={true}
|
|
724
|
-
/>
|
|
780
|
+
<SlotValue slot={slots.title} size="small"/>
|
|
725
781
|
</Typography>
|
|
726
782
|
) : (
|
|
727
783
|
<Typography component="div" variant="body2" className="font-semibold text-surface-500 dark:text-surface-400 font-mono text-xs transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400">
|
|
@@ -734,13 +790,7 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
734
790
|
{slots.subtitle && (
|
|
735
791
|
<div className="truncate mt-0.5">
|
|
736
792
|
<Typography variant="caption" component="div" className="text-surface-500 dark:text-surface-400 truncate">
|
|
737
|
-
<
|
|
738
|
-
propertyKey={slots.subtitle.propertyKey}
|
|
739
|
-
value={slots.subtitle.value}
|
|
740
|
-
property={slots.subtitle.property}
|
|
741
|
-
size="small"
|
|
742
|
-
textOnly={true}
|
|
743
|
-
/>
|
|
793
|
+
<SlotValue slot={slots.subtitle} size="small"/>
|
|
744
794
|
</Typography>
|
|
745
795
|
</div>
|
|
746
796
|
)}
|
|
@@ -820,23 +870,33 @@ const ListRow = React.memo(function ListRow<M extends Record<string, unknown>>({
|
|
|
820
870
|
})}
|
|
821
871
|
</>
|
|
822
872
|
) : (
|
|
823
|
-
/* ── SLOT MODE (editorial scanner layout) ──
|
|
873
|
+
/* ── SLOT MODE (editorial scanner layout) ──
|
|
874
|
+
The slots give up the row from the right inwards, the same
|
|
875
|
+
rule the columns follow above: whatever sits furthest from
|
|
876
|
+
the title is furthest from what the row is about, so it is
|
|
877
|
+
the first thing a narrowing row can afford to lose. Reading
|
|
878
|
+
the thresholds in source order gives the reverse of the
|
|
879
|
+
order they vanish in — they are written left to right, and
|
|
880
|
+
they disappear right to left. */
|
|
824
881
|
<div className="flex items-center gap-4 flex-shrink-0 ml-auto">
|
|
825
|
-
{/*
|
|
882
|
+
{/* TAGS slot — "free chips beside the status", so beside it.
|
|
883
|
+
Leftmost of the three, so the last to go. */}
|
|
884
|
+
{slots.tags && (
|
|
885
|
+
<div className="flex items-center gap-1 flex-shrink-0 @max-[360px]:hidden">
|
|
886
|
+
<TagChips slot={slots.tags} max={3}/>
|
|
887
|
+
</div>
|
|
888
|
+
)}
|
|
889
|
+
|
|
890
|
+
{/* STATUS slot */}
|
|
826
891
|
{slots.status && (
|
|
827
|
-
<div className="flex-shrink-0 @max-[
|
|
828
|
-
<
|
|
829
|
-
propertyKey={slots.status.propertyKey}
|
|
830
|
-
value={slots.status.value}
|
|
831
|
-
property={slots.status.property}
|
|
832
|
-
size="small"
|
|
833
|
-
/>
|
|
892
|
+
<div className="flex-shrink-0 @max-[460px]:hidden">
|
|
893
|
+
<SlotValue slot={slots.status} size="small"/>
|
|
834
894
|
</div>
|
|
835
895
|
)}
|
|
836
896
|
|
|
837
|
-
{/* DATE slot */}
|
|
897
|
+
{/* DATE slot — rightmost, so the first to go. */}
|
|
838
898
|
{slots.date && (
|
|
839
|
-
<div className="flex-shrink-0 text-right w-[80px] @max-[
|
|
899
|
+
<div className="flex-shrink-0 text-right w-[80px] @max-[560px]:hidden">
|
|
840
900
|
<Typography variant="caption" className="whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium">
|
|
841
901
|
{slots.date.formatted ?? "—"}
|
|
842
902
|
</Typography>
|
|
@@ -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,
|
|
@@ -687,7 +688,16 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
687
688
|
// Each child view gets a column that jumps to its tab. Keyed by the
|
|
688
689
|
// view's collection slug, which is the relation key — two relations
|
|
689
690
|
// to the same target used to collapse onto one column id here.
|
|
690
|
-
|
|
691
|
+
//
|
|
692
|
+
// Except where the relation was declared as a property and so already
|
|
693
|
+
// has a column showing the child rows: the button then repeated that
|
|
694
|
+
// column's heading verbatim. Filtered here as well as in
|
|
695
|
+
// `useColumnIds` so no id is displayed without a delegate to build it,
|
|
696
|
+
// and so the column picker does not offer the one back.
|
|
697
|
+
const redundant = getRedundantChildViewColumnIds(collectionWithLocalOrder);
|
|
698
|
+
const subcollectionsList = getEntityChildViews(collection)
|
|
699
|
+
.map(v => v.collection as AdminCollection)
|
|
700
|
+
.filter(child => !redundant.has(getSubcollectionColumnId(child)));
|
|
691
701
|
const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = subcollectionsList.map((subcollection: AdminCollection) => {
|
|
692
702
|
return {
|
|
693
703
|
key: getSubcollectionColumnId(subcollection),
|
|
@@ -720,7 +730,7 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
720
730
|
...(collection.additionalFields ?? EMPTY_ARRAY),
|
|
721
731
|
...subcollectionColumns
|
|
722
732
|
];
|
|
723
|
-
}, [collection, path, sidePanelController]);
|
|
733
|
+
}, [collection, collectionWithLocalOrder, path, sidePanelController]);
|
|
724
734
|
|
|
725
735
|
const updateLastDeleteTimestamp = useCallback(() => {
|
|
726
736
|
setLastDeleteTimestamp(Date.now());
|
|
@@ -823,6 +833,7 @@ parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
|
823
833
|
collection={collection}
|
|
824
834
|
filter={tableController.filterValues}
|
|
825
835
|
sortBy={tableController.sortBy}
|
|
836
|
+
searchString={tableController.searchString}
|
|
826
837
|
onCountChange={setDocsCount}
|
|
827
838
|
/>;
|
|
828
839
|
|
|
@@ -1126,13 +1137,18 @@ parentEntityIds,
|
|
|
1126
1137
|
|
|
1127
1138
|
{countFetcher}
|
|
1128
1139
|
|
|
1129
|
-
{
|
|
1140
|
+
{/* A plugin's error view takes priority over the built-in
|
|
1141
|
+
banner, the same precedence the empty state uses. This was
|
|
1142
|
+
computed above and then rendered nowhere, so `collection.error`
|
|
1143
|
+
— a declared, documented slot with its own props interface —
|
|
1144
|
+
did nothing at all, and a plugin author had no way to tell. */}
|
|
1145
|
+
{pluginErrorView ?? (tableController.dataLoadingError && tableController.data.length > 0 && (
|
|
1130
1146
|
<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
1147
|
<Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
|
|
1132
1148
|
<strong>Warning:</strong> {tableController.dataLoadingError.message || "Failed to update data."}
|
|
1133
1149
|
</Typography>
|
|
1134
1150
|
</div>
|
|
1135
|
-
)}
|
|
1151
|
+
))}
|
|
1136
1152
|
|
|
1137
1153
|
{/* When isSplitLayout, SplitListView is ALWAYS mounted — regardless
|
|
1138
1154
|
of viewMode. The toolbar + current view live in the left panel;
|
|
@@ -1302,17 +1318,26 @@ function DefaultCollectionEmptyState({
|
|
|
1302
1318
|
*/
|
|
1303
1319
|
const inflightCountRequests = new Map<string, Promise<number>>();
|
|
1304
1320
|
|
|
1305
|
-
function EntitiesCount({
|
|
1321
|
+
export function EntitiesCount({
|
|
1306
1322
|
path,
|
|
1307
1323
|
collection,
|
|
1308
1324
|
filter,
|
|
1309
1325
|
sortBy,
|
|
1326
|
+
searchString,
|
|
1310
1327
|
onCountChange
|
|
1311
1328
|
}: {
|
|
1312
1329
|
path: string,
|
|
1313
1330
|
collection: AdminCollection,
|
|
1314
1331
|
filter?: FilterValues<any>,
|
|
1315
1332
|
sortBy?: [string, "asc" | "desc"],
|
|
1333
|
+
/**
|
|
1334
|
+
* Required, not optional. The term sits in the same scope as the element
|
|
1335
|
+
* that mounts this and is passed to the toolbar and the empty state beside
|
|
1336
|
+
* it — it was simply not passed here, so a searched list showed the count
|
|
1337
|
+
* of the unsearched collection. A required prop is what stops the next
|
|
1338
|
+
* caller doing the same; there is no useful "count without the search".
|
|
1339
|
+
*/
|
|
1340
|
+
searchString: string | undefined,
|
|
1316
1341
|
onCountChange?: (count: number | null | undefined) => void,
|
|
1317
1342
|
}) {
|
|
1318
1343
|
|
|
@@ -1344,12 +1369,17 @@ function EntitiesCount({
|
|
|
1344
1369
|
const orderByParams: [string, "asc" | "desc"] | undefined = sortByProperty && currentSort ? [String(sortByProperty), currentSort] : undefined;
|
|
1345
1370
|
|
|
1346
1371
|
// Deduplicate inflight count requests (e.g. React StrictMode double-mount)
|
|
1347
|
-
|
|
1372
|
+
// The search term is part of the query, so it is part of the key. The
|
|
1373
|
+
// cache is module-level and outlives an unmount, so a key that omits it
|
|
1374
|
+
// does not merely lose precision — it answers one search with a
|
|
1375
|
+
// different search's total.
|
|
1376
|
+
const cacheKey = `${path}|${filterKey}|${sortByProperty ?? ""}|${currentSort ?? ""}|${searchString ?? ""}`;
|
|
1348
1377
|
let countPromise = inflightCountRequests.get(cacheKey);
|
|
1349
1378
|
if (!countPromise) {
|
|
1350
1379
|
countPromise = accessor.count({
|
|
1351
1380
|
where: whereParams,
|
|
1352
|
-
orderBy: orderByParams
|
|
1381
|
+
orderBy: orderByParams,
|
|
1382
|
+
searchString
|
|
1353
1383
|
});
|
|
1354
1384
|
inflightCountRequests.set(cacheKey, countPromise);
|
|
1355
1385
|
// Clean up the inflight entry once resolved/rejected
|
|
@@ -1364,7 +1394,7 @@ function EntitiesCount({
|
|
|
1364
1394
|
});
|
|
1365
1395
|
|
|
1366
1396
|
return () => { cancelled = true; };
|
|
1367
|
-
}, [path, filterKey, sortByProperty, currentSort]);
|
|
1397
|
+
}, [path, filterKey, sortByProperty, currentSort, searchString]);
|
|
1368
1398
|
|
|
1369
1399
|
// Count is now displayed in the breadcrumb bar, this component only fetches and reports
|
|
1370
1400
|
return null;
|
|
@@ -16,7 +16,8 @@ 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 { SlotValue, TagChips } from "./SlotValue";
|
|
20
21
|
|
|
21
22
|
export type EntityCardBindingProps<M extends Record<string, unknown> = Record<string, unknown>> = {
|
|
22
23
|
entity: Entity<M>;
|
|
@@ -56,13 +57,10 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
56
57
|
customizationController.propertyConfigs
|
|
57
58
|
);
|
|
58
59
|
|
|
59
|
-
const slots =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
slotKeys
|
|
64
|
-
),
|
|
65
|
-
[entity, collection, slotKeys]
|
|
60
|
+
const slots = useEntitySlots(
|
|
61
|
+
entity as Entity<Record<string, unknown>>,
|
|
62
|
+
collection as AdminCollection<Record<string, unknown>>,
|
|
63
|
+
slotKeys
|
|
66
64
|
);
|
|
67
65
|
|
|
68
66
|
const handleClick = (e?: React.MouseEvent) => {
|
|
@@ -104,13 +102,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
104
102
|
<div className="aspect-[4/3] relative overflow-hidden bg-surface-100 dark:bg-surface-900">
|
|
105
103
|
{slots.image ? (
|
|
106
104
|
<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
|
-
/>
|
|
105
|
+
<SlotValue slot={slots.image} size="medium" fill={true}/>
|
|
114
106
|
</div>
|
|
115
107
|
) : (
|
|
116
108
|
<div className="w-full h-full flex items-center justify-center">
|
|
@@ -160,12 +152,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
160
152
|
{/* Title slot */}
|
|
161
153
|
<div className="truncate my-1 text-sm font-medium min-h-[20px]">
|
|
162
154
|
{slots.title ? (
|
|
163
|
-
<
|
|
164
|
-
propertyKey={slots.title.propertyKey}
|
|
165
|
-
value={slots.title.value}
|
|
166
|
-
property={slots.title.property}
|
|
167
|
-
size="small"
|
|
168
|
-
/>
|
|
155
|
+
<SlotValue slot={slots.title} size="small"/>
|
|
169
156
|
) : (
|
|
170
157
|
<Typography variant="body2" className="text-surface-500">
|
|
171
158
|
{entity.id}
|
|
@@ -176,12 +163,7 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
176
163
|
{/* Subtitle slot */}
|
|
177
164
|
{slots.subtitle && (
|
|
178
165
|
<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
|
-
propertyKey={slots.subtitle.propertyKey}
|
|
181
|
-
value={slots.subtitle.value}
|
|
182
|
-
property={slots.subtitle.property}
|
|
183
|
-
size="small"
|
|
184
|
-
/>
|
|
166
|
+
<SlotValue slot={slots.subtitle} size="small"/>
|
|
185
167
|
</div>
|
|
186
168
|
)}
|
|
187
169
|
|
|
@@ -208,15 +190,20 @@ export function EntityCardBinding<M extends Record<string, unknown> = Record<str
|
|
|
208
190
|
</div>
|
|
209
191
|
)}
|
|
210
192
|
|
|
211
|
-
{/* Status slot */}
|
|
212
|
-
{slots.status && (
|
|
213
|
-
<div className="mt-1">
|
|
214
|
-
<
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
193
|
+
{/* Status + tags slot — "free chips beside the status" */}
|
|
194
|
+
{(slots.status || slots.tags) && (
|
|
195
|
+
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
|
196
|
+
{slots.status && <SlotValue slot={slots.status} size="small"/>}
|
|
197
|
+
{slots.tags && <TagChips slot={slots.tags}/>}
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Date slot — a card is a list row with the image on top, and the
|
|
202
|
+
row has always shown it. It was the one slot the card resolved
|
|
203
|
+
and never rendered. */}
|
|
204
|
+
{slots.date && (
|
|
205
|
+
<div className="mt-1 text-[10px] text-surface-500 dark:text-surface-400">
|
|
206
|
+
{slots.date.formatted}
|
|
220
207
|
</div>
|
|
221
208
|
)}
|
|
222
209
|
</div>
|