@rebasepro/admin 0.13.0 → 0.13.1-canary.g1822133
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-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-W8I3YV89.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BsLTNoqX.js → RouterCollectionsStudioView-CwTpdzWF.js} +9 -13
- package/dist/RouterCollectionsStudioView-CwTpdzWF.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 +21 -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/editor.js +3 -0
- package/dist/editor.js.map +1 -1
- package/dist/{export-J79rMoNj.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.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-D5SKygpJ.js} +4 -4
- package/dist/{history-D7L_JlLO.js.map → history-D5SKygpJ.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-C_4edkoD.js} +6 -5
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +237 -123
- 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/components/date_fns_locales.d.ts +9 -0
- 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-C-D5yD2n.js} +4161 -1809
- package/dist/util-C-D5yD2n.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/AICollectionGeneratorPopover.tsx +8 -3
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +5 -2
- package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +1 -0
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +9 -8
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +9 -11
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +5 -2
- 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/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +1 -0
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +92 -84
- 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 +52 -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 +26 -23
- package/src/components/DetailViewBinding.tsx +109 -62
- package/src/components/DrawerNavigationGroup.tsx +16 -39
- package/src/components/DrawerNavigationItem.tsx +56 -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 +76 -15
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- 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/DataNewPropertiesMapping.tsx +20 -3
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +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 +77 -6
- 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/editor/components/image-bubble.tsx +2 -0
- package/src/editor/selectors/link-selector.tsx +1 -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 +11 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/GeopointFieldBinding.tsx +23 -16
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- 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/TextFieldBinding.tsx +18 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +4 -1
- package/src/form/field_bindings/VectorFieldBinding.tsx +7 -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/DatePreview.tsx +15 -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/components/date_fns_locales.ts +125 -0
- 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/CollectionEditorDialog-CjXkWZ64.js.map +0 -1
- 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
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SearchIcon,
|
|
19
19
|
Separator,
|
|
20
20
|
Tooltip,
|
|
21
|
-
|
|
21
|
+
usePortalContainer,
|
|
22
22
|
XIcon
|
|
23
23
|
} from "@rebasepro/ui";
|
|
24
24
|
import * as React from "react";
|
|
@@ -29,7 +29,6 @@ import { EntityPreviewBindingData } from "./EntityPreviewBinding";
|
|
|
29
29
|
import { useData, useRelationSelector } from "@rebasepro/app";
|
|
30
30
|
import { useSidePanel } from "../hooks/useSidePanel";
|
|
31
31
|
import { normalizeToEntityRelation } from "@rebasepro/common";
|
|
32
|
-
import { EmptyValue } from "../preview";
|
|
33
32
|
|
|
34
33
|
/** Whether an authored relation yields many rows. Derived from its kind. */
|
|
35
34
|
function relationCardinality(relation: { kind?: string; cardinality?: string } | undefined): "one" | "many" | undefined {
|
|
@@ -58,7 +57,14 @@ export interface RelationSelectorProps {
|
|
|
58
57
|
|
|
59
58
|
onValueChange?: (updatedValue: EntityRelation | EntityRelation[] | undefined) => void;
|
|
60
59
|
placeholder?: React.ReactNode;
|
|
61
|
-
|
|
60
|
+
/**
|
|
61
|
+
* `large` is the form's size and is on the shared 28/32/40/48 control
|
|
62
|
+
* scale, so a relation picker lines up with the text field beside it.
|
|
63
|
+
*
|
|
64
|
+
* `small` and `medium` predate that scale (42 and 56) and are kept as they
|
|
65
|
+
* are because the table cells and the filter row are built around them.
|
|
66
|
+
*/
|
|
67
|
+
size?: "small" | "medium" | "large";
|
|
62
68
|
useChips?: boolean;
|
|
63
69
|
disabled?: boolean;
|
|
64
70
|
error?: boolean; // kept for backwards compatibility (could be used for styling later)
|
|
@@ -84,7 +90,20 @@ export interface RelationSelectorProps {
|
|
|
84
90
|
pageSize?: number;
|
|
85
91
|
emptyPlaceholder?: string;
|
|
86
92
|
searchPlaceholder?: string;
|
|
93
|
+
/** Shown in the open list when a search matched nothing. */
|
|
87
94
|
noResultsText?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Shown on the closed trigger when nothing is selected. Says only that the
|
|
97
|
+
* field is empty — see the note by `resolvedPlaceholder` for why it cannot
|
|
98
|
+
* say whether there is anything to pick.
|
|
99
|
+
*/
|
|
100
|
+
emptyText?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Shown in the open list when the target collection turned out to hold no
|
|
103
|
+
* rows at all, as opposed to a search simply not matching. Defaults to a
|
|
104
|
+
* sentence naming the target collection.
|
|
105
|
+
*/
|
|
106
|
+
emptyCollectionText?: string;
|
|
88
107
|
loadingText?: string;
|
|
89
108
|
}
|
|
90
109
|
|
|
@@ -108,7 +127,9 @@ export const RelationSelector = React.forwardRef<
|
|
|
108
127
|
pageSize,
|
|
109
128
|
emptyPlaceholder,
|
|
110
129
|
searchPlaceholder = "Search...",
|
|
111
|
-
noResultsText = "No
|
|
130
|
+
noResultsText = "No matches.",
|
|
131
|
+
emptyText = "Select…",
|
|
132
|
+
emptyCollectionText,
|
|
112
133
|
loadingText = "Loading..."
|
|
113
134
|
},
|
|
114
135
|
ref
|
|
@@ -117,10 +138,16 @@ export const RelationSelector = React.forwardRef<
|
|
|
117
138
|
const collection = relation.target();
|
|
118
139
|
const dataClient = useData();
|
|
119
140
|
const sidePanelController = useSidePanel();
|
|
141
|
+
const contextPortalContainer = usePortalContainer();
|
|
120
142
|
const multiple = multipleOverride ?? relationCardinality(relation) === "many";
|
|
121
143
|
|
|
122
144
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
123
145
|
const isPopoverOpenRef = useRef(false);
|
|
146
|
+
// The list is only worth fetching once the picker has been opened. A
|
|
147
|
+
// table cell mounts one of these per row, and the selected value is
|
|
148
|
+
// resolved by its own effect below, not from the list. Latches on the
|
|
149
|
+
// first open so a closed popover keeps its results.
|
|
150
|
+
const [listRequested, setListRequested] = useState(false);
|
|
124
151
|
const [selectedItems, setSelectedItems] = useState<RelationItem[]>([]);
|
|
125
152
|
const [isLoadingSelectedItems, setIsLoadingSelectedItems] = useState(false);
|
|
126
153
|
const [searchString, setSearchString] = useState<string>("");
|
|
@@ -143,7 +170,8 @@ export const RelationSelector = React.forwardRef<
|
|
|
143
170
|
path: getCollectionDataPath(collection),
|
|
144
171
|
collection,
|
|
145
172
|
fixedFilter,
|
|
146
|
-
pageSize
|
|
173
|
+
pageSize,
|
|
174
|
+
enabled: listRequested
|
|
147
175
|
});
|
|
148
176
|
|
|
149
177
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
@@ -410,6 +438,7 @@ relation } as RelationItem;
|
|
|
410
438
|
if (next) {
|
|
411
439
|
// Capture current selection so we can pin those items to the top of the list
|
|
412
440
|
pinnedIdsRef.current = new Set(selectedItems.map(i => String(i.id)));
|
|
441
|
+
setListRequested(true);
|
|
413
442
|
setIsPopoverOpen(true);
|
|
414
443
|
isPopoverOpenRef.current = true;
|
|
415
444
|
}
|
|
@@ -448,18 +477,35 @@ relation } as RelationItem;
|
|
|
448
477
|
};
|
|
449
478
|
}, [isPopoverOpen]);
|
|
450
479
|
|
|
451
|
-
useInjectStyles("RelationSelector", " [cmdk-group] { max-height: 40vh; overflow-y: auto; } ");
|
|
452
|
-
|
|
453
480
|
const closePopover = useCallback(() => {
|
|
454
481
|
setIsPopoverOpen(false);
|
|
455
482
|
isPopoverOpenRef.current = false;
|
|
456
483
|
pinnedIdsRef.current = null;
|
|
457
484
|
}, []);
|
|
458
485
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
//
|
|
462
|
-
|
|
486
|
+
// Text, not `EmptyValue`. That component draws a small grey rounded bar
|
|
487
|
+
// with no text in it, which is the same shape the `Skeleton` component
|
|
488
|
+
// uses while data is in flight — so a relation you simply had not filled
|
|
489
|
+
// in was indistinguishable from one that was still loading, and an empty
|
|
490
|
+
// Author or Tags field read as permanently stuck. It also left the trigger
|
|
491
|
+
// button with no accessible name, since the bar contains no text at all.
|
|
492
|
+
//
|
|
493
|
+
// This says "not filled in" rather than "nothing to choose from": which of
|
|
494
|
+
// those it is cannot be known here. The list is deliberately not fetched
|
|
495
|
+
// until the picker is first opened (`enabled: listRequested`, guarded by
|
|
496
|
+
// relation_cell_picker_gating.test.tsx, because a table renders one of
|
|
497
|
+
// these per row), so before that `availableItems` is empty because nobody
|
|
498
|
+
// asked, not because the target collection is. The genuine "nothing to
|
|
499
|
+
// pick yet" state is reported inside the popover below, where the fetch
|
|
500
|
+
// has actually happened.
|
|
501
|
+
const resolvedPlaceholder = placeholder || emptyPlaceholder || emptyText;
|
|
502
|
+
|
|
503
|
+
// Whatever dialog, side dialog or sheet we are inside offers as its
|
|
504
|
+
// portal host, falling back to `document.body`. This is not cosmetic:
|
|
505
|
+
// those are modal, and a modal locks scrolling everywhere outside its
|
|
506
|
+
// own content — a list portaled to the body opens and then refuses to
|
|
507
|
+
// scroll, because every wheel event over it is cancelled.
|
|
508
|
+
const portalContainer = (contextPortalContainer ?? (typeof document !== "undefined" ? document.body : undefined)) ?? undefined;
|
|
463
509
|
|
|
464
510
|
return (
|
|
465
511
|
<>
|
|
@@ -474,6 +520,10 @@ relation } as RelationItem;
|
|
|
474
520
|
disabled={disabled}
|
|
475
521
|
onClick={() => {
|
|
476
522
|
if (disabled) return;
|
|
523
|
+
// Unconditional: the toggle can only close a
|
|
524
|
+
// popover that was opened, which requested the
|
|
525
|
+
// list already.
|
|
526
|
+
setListRequested(true);
|
|
477
527
|
setIsPopoverOpen(o => {
|
|
478
528
|
const next = !o;
|
|
479
529
|
isPopoverOpenRef.current = next;
|
|
@@ -488,9 +538,13 @@ relation } as RelationItem;
|
|
|
488
538
|
className={cls(
|
|
489
539
|
{
|
|
490
540
|
"min-h-[42px] py-1 px-2": size === "small",
|
|
491
|
-
"min-h-[56px] py-2 px-4": size === "medium"
|
|
541
|
+
"min-h-[56px] py-2 px-4": size === "medium",
|
|
542
|
+
"min-h-[48px] py-1 px-4": size === "large"
|
|
492
543
|
},
|
|
493
|
-
|
|
544
|
+
// `rounded-lg` like every other field box —
|
|
545
|
+
// `rounded-md` made this the one control in a
|
|
546
|
+
// row with a different corner.
|
|
547
|
+
"w-full select-none rounded-lg text-sm relative flex items-center",
|
|
494
548
|
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
495
549
|
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
496
550
|
className
|
|
@@ -580,7 +634,7 @@ relation } as RelationItem;
|
|
|
580
634
|
</Tooltip>
|
|
581
635
|
)}
|
|
582
636
|
<ChevronDownIcon
|
|
583
|
-
size={size === "
|
|
637
|
+
size={size === "small" ? iconSize.small : iconSize.medium}
|
|
584
638
|
className={cls("transition", isPopoverOpen ? "rotate-180" : "")}
|
|
585
639
|
/>
|
|
586
640
|
</div>
|
|
@@ -652,10 +706,17 @@ relation } as RelationItem;
|
|
|
652
706
|
className="ml-2 text-sm text-text-secondary dark:text-text-secondary-dark">{loadingText}</span>
|
|
653
707
|
</div>
|
|
654
708
|
)}
|
|
709
|
+
{/* Two different nothings, which used to share one
|
|
710
|
+
message ("No relations found.") even though the
|
|
711
|
+
fix for each is the opposite: clear the search, or
|
|
712
|
+
go and create the first row. With the fetch done,
|
|
713
|
+
the search string is what tells them apart. */}
|
|
655
714
|
{!isLoading && availableItems.length === 0 && (
|
|
656
715
|
<CommandPrimitive.Empty
|
|
657
716
|
className="px-4 py-6 text-center text-text-secondary dark:text-text-secondary-dark">
|
|
658
|
-
{
|
|
717
|
+
{searchString
|
|
718
|
+
? noResultsText
|
|
719
|
+
: (emptyCollectionText ?? `No ${collection.name ?? "entries"} yet.`)}
|
|
659
720
|
</CommandPrimitive.Empty>
|
|
660
721
|
)}
|
|
661
722
|
<CommandPrimitive.Group>
|
|
@@ -4,9 +4,8 @@ import {
|
|
|
4
4
|
coolIconKeys,
|
|
5
5
|
debounce,
|
|
6
6
|
IconButton,
|
|
7
|
-
iconKeys,
|
|
8
7
|
iconSize,
|
|
9
|
-
|
|
8
|
+
LucideIconByName,
|
|
10
9
|
SearchBar,
|
|
11
10
|
Tooltip
|
|
12
11
|
} from "@rebasepro/ui";
|
|
@@ -15,20 +14,6 @@ import { useTranslation } from "@rebasepro/app";
|
|
|
15
14
|
|
|
16
15
|
const UPDATE_SEARCH_INDEX_WAIT_MS = 220;
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
function toPascalCase(str: string): string {
|
|
20
|
-
return str.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function resolveIcon(iconKey: string): React.ComponentType<{ size: number }> | null {
|
|
24
|
-
const allIcons = lucideIcons as Record<string, React.ComponentType<{ size: number }>>;
|
|
25
|
-
let icon = allIcons[iconKey];
|
|
26
|
-
if (!icon) {
|
|
27
|
-
icon = allIcons[toPascalCase(iconKey)];
|
|
28
|
-
}
|
|
29
|
-
return icon ?? null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
17
|
export interface SearchIconsProps {
|
|
33
18
|
selectedIcon?: string;
|
|
34
19
|
onIconSelected?: (icon: string) => void;
|
|
@@ -74,23 +59,19 @@ export function SearchIconsView({
|
|
|
74
59
|
/>
|
|
75
60
|
|
|
76
61
|
<div className={"flex max-w-full flex-wrap mt-4"}>
|
|
77
|
-
{icons.map((icon: string) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</IconButton>
|
|
91
|
-
</Tooltip>
|
|
92
|
-
);
|
|
93
|
-
})}
|
|
62
|
+
{icons.map((icon: string) => (
|
|
63
|
+
<Tooltip title={icon} key={icon}
|
|
64
|
+
asChild={true}>
|
|
65
|
+
<IconButton
|
|
66
|
+
shape={"square"}
|
|
67
|
+
toggled={selectedIcon === icon}
|
|
68
|
+
onClick={onIconSelected ? () => onIconSelected(icon) : undefined}
|
|
69
|
+
className="box-content m-1"
|
|
70
|
+
>
|
|
71
|
+
<LucideIconByName name={icon} size={iconSize.medium}/>
|
|
72
|
+
</IconButton>
|
|
73
|
+
</Tooltip>
|
|
74
|
+
))}
|
|
94
75
|
</div>
|
|
95
76
|
</>
|
|
96
77
|
);
|
|
@@ -190,6 +190,7 @@ export function StringNumberFilterField({
|
|
|
190
190
|
|
|
191
191
|
{!enumValues && <TextField
|
|
192
192
|
size={"medium"}
|
|
193
|
+
aria-label={`${title ?? name} filter value`}
|
|
193
194
|
type={type === "number" ? "number" : undefined}
|
|
194
195
|
value={internalValue !== undefined && internalValue != null ? String(internalValue) : ""}
|
|
195
196
|
disabled={isNullOperation}
|
|
@@ -91,7 +91,17 @@ function SideDialogView({
|
|
|
91
91
|
const [blocked, setBlocked] = useState(false);
|
|
92
92
|
const [blockedNavigationMessage, setBlockedNavigationMessage] = useState<React.ReactNode | undefined>();
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// Held in a ref, read through a getter, because of *when* it is written and
|
|
95
|
+
// read. "Save and close" raises it and submits the form in one click
|
|
96
|
+
// handler, and the callback that acts on it — the form's `onSaved` — was
|
|
97
|
+
// captured before React could re-render with the new value. As state the
|
|
98
|
+
// flag was therefore always read one save late: the first "save and close"
|
|
99
|
+
// saved and left the panel open, and the *next* save closed it.
|
|
100
|
+
// Nothing renders from it, so a ref loses nothing.
|
|
101
|
+
const pendingCloseRef = React.useRef(false);
|
|
102
|
+
const setPendingClose = useCallback((value: boolean) => {
|
|
103
|
+
pendingCloseRef.current = value;
|
|
104
|
+
}, []);
|
|
95
105
|
|
|
96
106
|
const widthRef = React.useRef<string | undefined>(panel?.width);
|
|
97
107
|
const width = widthRef.current;
|
|
@@ -142,9 +152,11 @@ function SideDialogView({
|
|
|
142
152
|
setBlockedNavigationMessage,
|
|
143
153
|
width,
|
|
144
154
|
close: onCloseRequest,
|
|
145
|
-
pendingClose
|
|
155
|
+
get pendingClose() {
|
|
156
|
+
return pendingCloseRef.current;
|
|
157
|
+
},
|
|
146
158
|
setPendingClose
|
|
147
|
-
}), [blocked, setBlockedNavigationMessage, width, onCloseRequest,
|
|
159
|
+
}), [blocked, setBlockedNavigationMessage, width, onCloseRequest, setPendingClose]);
|
|
148
160
|
|
|
149
161
|
const additionalProps = panel?.additional as SidePanelBindingProps | undefined;
|
|
150
162
|
const isDialogMode = additionalProps?.collection?.openEntityMode === "dialog";
|
|
@@ -9,7 +9,7 @@ import { EditViewBinding } from "./EditViewBinding";
|
|
|
9
9
|
import { DetailViewBinding } from "./DetailViewBinding";
|
|
10
10
|
import { useSideDialogContext } from "./SideDialogs";
|
|
11
11
|
import { useNavigate } from "react-router";
|
|
12
|
-
import {
|
|
12
|
+
import { useComponentOverride } from "@rebasepro/app";
|
|
13
13
|
import { useCollectionRegistryController } from "../hooks/navigation/contexts/CollectionRegistryContext";
|
|
14
14
|
import { useSidePanel } from "../hooks/useSidePanel";
|
|
15
15
|
import { useUrlController } from "../hooks/navigation/contexts/UrlContext";
|
|
@@ -49,21 +49,60 @@ export function SidePanelBinding(props: SidePanelBindingProps) {
|
|
|
49
49
|
const sideDialogsController = useSideDialogContext();
|
|
50
50
|
const urlController = useUrlController();
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The X in the identity bar.
|
|
54
|
+
*
|
|
55
|
+
* Unforced, so it stops at the same unsaved-changes prompt that Escape and
|
|
56
|
+
* a click outside already do. It used to clear `blocked` and force the
|
|
57
|
+
* close, which made the one control actually labelled "close" the only way
|
|
58
|
+
* out of the panel that dropped an edit without asking.
|
|
59
|
+
*
|
|
60
|
+
* `props.onClose` is not called here: the panel carries the same callback
|
|
61
|
+
* (see `propsToSidePanel`) and {@link SideDialogs} fires it once the panel
|
|
62
|
+
* really closes — which is after the prompt, not before it.
|
|
63
|
+
*/
|
|
52
64
|
const onClose = () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
65
|
+
close();
|
|
66
|
+
}
|
|
56
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Closing because the record was just saved, which is the one case that has
|
|
70
|
+
* to force. `blocked` is driven by an effect on the form's dirty flag and is
|
|
71
|
+
* still true for a tick after a save; prompting about unsaved changes here
|
|
72
|
+
* would be asking about values that are already on the server.
|
|
73
|
+
*/
|
|
74
|
+
const closeAfterSave = () => {
|
|
57
75
|
setBlocked(false);
|
|
58
76
|
close(true);
|
|
59
77
|
}
|
|
60
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The panel's response to a save, and the one place it decides where the
|
|
81
|
+
* panel goes next.
|
|
82
|
+
*
|
|
83
|
+
* Exactly **one** panel navigation is raised here. These branches used to
|
|
84
|
+
* run independently — a `replace` onto the saved record's address *and* a
|
|
85
|
+
* close — and because the last navigation is the one that survives, which
|
|
86
|
+
* of them the user got was settled by statement order rather than by
|
|
87
|
+
* intent. Closing wins by construction now: moving a panel to a new address
|
|
88
|
+
* it is about to leave is work whose only effect is to fight the close.
|
|
89
|
+
*
|
|
90
|
+
* `props.onUpdate` runs last for the same reason. It belongs to whoever
|
|
91
|
+
* opened the panel and it is free to navigate — the reference picker's
|
|
92
|
+
* selects the record and closes the picker — so the opener's intent should
|
|
93
|
+
* be the final word, not something this panel overwrites on its way out.
|
|
94
|
+
*/
|
|
61
95
|
const onUpdate = (params: OnUpdateParams) => {
|
|
62
|
-
if (props.onUpdate) {
|
|
63
|
-
props.onUpdate(params);
|
|
64
|
-
}
|
|
65
96
|
setEditInDialog(false);
|
|
66
|
-
|
|
97
|
+
|
|
98
|
+
// Either "save and close" was clicked, or the opener asked for it when
|
|
99
|
+
// it opened the panel.
|
|
100
|
+
const closeRequested = sideDialogsController.pendingClose || props.closeOnSave;
|
|
101
|
+
|
|
102
|
+
if (closeRequested) {
|
|
103
|
+
sideDialogsController.setPendingClose(false);
|
|
104
|
+
closeAfterSave();
|
|
105
|
+
} else if (params.status !== "existing") {
|
|
67
106
|
// A newly created entity replaces the panel with its own address,
|
|
68
107
|
// which already leaves the edit view behind.
|
|
69
108
|
sidePanelController.replace({
|
|
@@ -77,11 +116,7 @@ export function SidePanelBinding(props: SidePanelBindingProps) {
|
|
|
77
116
|
closeEditView();
|
|
78
117
|
}
|
|
79
118
|
|
|
80
|
-
|
|
81
|
-
sideDialogsController.setPendingClose(false);
|
|
82
|
-
onClose();
|
|
83
|
-
}
|
|
84
|
-
|
|
119
|
+
props.onUpdate?.(params);
|
|
85
120
|
}
|
|
86
121
|
|
|
87
122
|
const parentCollectionSlugs = useMemo(() => {
|
|
@@ -181,6 +216,43 @@ entityId }
|
|
|
181
216
|
return <div className={"w-full"}/>;
|
|
182
217
|
}
|
|
183
218
|
|
|
219
|
+
const closeButton = (
|
|
220
|
+
<IconButton
|
|
221
|
+
className="self-center"
|
|
222
|
+
size={"small"}
|
|
223
|
+
onClick={onClose}>
|
|
224
|
+
<XIcon/>
|
|
225
|
+
</IconButton>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Where the panel's own two controls sit, which is not the same question in
|
|
230
|
+
* the two layouts that have them.
|
|
231
|
+
*
|
|
232
|
+
* A **side panel** slides in over the list from the right, and its leading
|
|
233
|
+
* edge is the seam between the two. Close belongs on that seam — it points
|
|
234
|
+
* back at the thing still visible behind it — and it is where the eye
|
|
235
|
+
* already is, because the breadcrumb starts there.
|
|
236
|
+
*
|
|
237
|
+
* A **dialog** has no seam and nothing behind it to point at. It is a box
|
|
238
|
+
* centred on a dimmed screen, and every box like it on every platform is
|
|
239
|
+
* dismissed from its top-right corner. Close was sitting at the far left
|
|
240
|
+
* of the bar, a full dialog's width from the expand button it belongs
|
|
241
|
+
* beside, in the one corner that means "back" in a layout with no back.
|
|
242
|
+
*
|
|
243
|
+
* So in a dialog the two travel together at the trailing edge, expand then
|
|
244
|
+
* close: the window controls, after the record's actions and its overflow
|
|
245
|
+
* menu, in the corner a dialog is closed from.
|
|
246
|
+
*/
|
|
247
|
+
const barActionsStart = isDialogMode ? undefined : closeButton;
|
|
248
|
+
|
|
249
|
+
const windowControls = (expandButton: React.ReactNode) => (
|
|
250
|
+
<div className="flex gap-1 items-center">
|
|
251
|
+
{expandButton}
|
|
252
|
+
{isDialogMode && closeButton}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
|
|
184
256
|
return (
|
|
185
257
|
<>
|
|
186
258
|
<ErrorBoundary>
|
|
@@ -193,31 +265,20 @@ entityId }
|
|
|
193
265
|
parentCollectionSlugs={parentCollectionSlugs}
|
|
194
266
|
parentEntityIds={parentEntityIds}
|
|
195
267
|
onEditClick={openEditView}
|
|
196
|
-
barActionsStart={
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<IconButton
|
|
268
|
+
barActionsStart={barActionsStart}
|
|
269
|
+
barActions={() => windowControls(
|
|
270
|
+
allowFullScreen && <IconButton
|
|
200
271
|
className="self-center"
|
|
201
272
|
size={"small"}
|
|
202
|
-
onClick={
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
size={"small"}
|
|
212
|
-
onClick={() => {
|
|
213
|
-
if (entityId) {
|
|
214
|
-
const fullScreenUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}`);
|
|
215
|
-
navigate(fullScreenUrl, { state: null });
|
|
216
|
-
}
|
|
217
|
-
}}>
|
|
218
|
-
<Maximize2Icon/>
|
|
219
|
-
</IconButton>}
|
|
220
|
-
</div>}
|
|
273
|
+
onClick={() => {
|
|
274
|
+
if (entityId) {
|
|
275
|
+
const fullScreenUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}`);
|
|
276
|
+
navigate(fullScreenUrl, { state: null });
|
|
277
|
+
}
|
|
278
|
+
}}>
|
|
279
|
+
<Maximize2Icon/>
|
|
280
|
+
</IconButton>
|
|
281
|
+
)}
|
|
221
282
|
onTabChange={({
|
|
222
283
|
entityId: tabEntityId,
|
|
223
284
|
selectedTab,
|
|
@@ -245,38 +306,33 @@ entityId }
|
|
|
245
306
|
onValuesModified={onValuesModified}
|
|
246
307
|
onSaved={onUpdate}
|
|
247
308
|
navigateBack={closeEditView}
|
|
248
|
-
barActionsStart={
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<IconButton
|
|
309
|
+
barActionsStart={barActionsStart}
|
|
310
|
+
barActions={({ carryEdit }) => windowControls(
|
|
311
|
+
allowFullScreen && <IconButton
|
|
252
312
|
className="self-center"
|
|
253
313
|
size={"small"}
|
|
254
|
-
onClick={
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}}>
|
|
277
|
-
<Maximize2Icon/>
|
|
278
|
-
</IconButton>}
|
|
279
|
-
</div>}
|
|
314
|
+
onClick={() => {
|
|
315
|
+
// The form's own handoff, rather than a second
|
|
316
|
+
// copy of the rules for it here: only an edit in
|
|
317
|
+
// progress is worth carrying (a clean record
|
|
318
|
+
// stashed anyway arrived announcing unsaved
|
|
319
|
+
// changes to someone who had only looked at it),
|
|
320
|
+
// and only the fields that were edited (carrying
|
|
321
|
+
// the whole record marked every field touched).
|
|
322
|
+
carryEdit?.();
|
|
323
|
+
setBlocked(false);
|
|
324
|
+
setBlockedNavigationMessage(undefined);
|
|
325
|
+
if (entityId) {
|
|
326
|
+
const fullScreenUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}`);
|
|
327
|
+
navigate(fullScreenUrl, { state: null });
|
|
328
|
+
} else {
|
|
329
|
+
const fullScreenUrl = urlController.buildUrlCollectionPath(path);
|
|
330
|
+
navigate(fullScreenUrl + "#new", { state: null });
|
|
331
|
+
}
|
|
332
|
+
}}>
|
|
333
|
+
<Maximize2Icon/>
|
|
334
|
+
</IconButton>
|
|
335
|
+
)}
|
|
280
336
|
onTabChange={({
|
|
281
337
|
entityId: tabEntityId,
|
|
282
338
|
selectedTab,
|