@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
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
PopoverPrimitive,
|
|
15
15
|
SearchIcon,
|
|
16
16
|
Separator,
|
|
17
|
-
|
|
17
|
+
usePortalContainer,
|
|
18
18
|
XIcon
|
|
19
19
|
} from "@rebasepro/ui";
|
|
20
20
|
import * as React from "react";
|
|
@@ -23,6 +23,7 @@ import { Command as CommandPrimitive } from "cmdk";
|
|
|
23
23
|
import { User } from "@rebasepro/types";
|
|
24
24
|
import { apiBaseOf, useRebaseClient, useAuthController, UserDisplay } from "@rebasepro/app";
|
|
25
25
|
import { EmptyValue } from "../preview";
|
|
26
|
+
import { useResolvedUser } from "../hooks/useResolvedUsers";
|
|
26
27
|
|
|
27
28
|
interface UserSelectorItem {
|
|
28
29
|
uid: string;
|
|
@@ -46,10 +47,19 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE }: { pageSize?: number }
|
|
|
46
47
|
const [currentSearch, setCurrentSearch] = useState("");
|
|
47
48
|
const offsetRef = useRef(0);
|
|
48
49
|
const userCacheRef = useRef<Map<string, User>>(new Map());
|
|
50
|
+
// Stamped by every request; only the newest one is allowed to write state.
|
|
51
|
+
// Typing "alice" issues one request per keystroke and nothing cancels them,
|
|
52
|
+
// so the response for "al" landing after the response for "alice" replaced
|
|
53
|
+
// the list — and `offsetRef` — with results for a string the user had
|
|
54
|
+
// already moved past. Same shape as `useRelationSelector`.
|
|
55
|
+
const requestIdRef = useRef(0);
|
|
56
|
+
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
49
57
|
|
|
50
58
|
const fetchUsers = useCallback(async (searchStr: string, offset: number, append: boolean) => {
|
|
51
59
|
const apiBase = apiBaseOf(client);
|
|
52
60
|
if (!apiBase) return;
|
|
61
|
+
const requestId = ++requestIdRef.current;
|
|
62
|
+
const superseded = () => requestId !== requestIdRef.current;
|
|
53
63
|
setIsLoading(true);
|
|
54
64
|
try {
|
|
55
65
|
const token = await getAuthToken?.();
|
|
@@ -68,6 +78,7 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE }: { pageSize?: number }
|
|
|
68
78
|
});
|
|
69
79
|
if (!response.ok) throw new Error("Failed to fetch users");
|
|
70
80
|
const data = await response.json();
|
|
81
|
+
if (superseded()) return;
|
|
71
82
|
const rows: Record<string, unknown>[] = data.data ?? data.users ?? [];
|
|
72
83
|
const newItems: UserSelectorItem[] = rows.map((r: Record<string, unknown>) => {
|
|
73
84
|
const user: User = {
|
|
@@ -88,9 +99,10 @@ user };
|
|
|
88
99
|
setHasMore(newItems.length >= pageSize);
|
|
89
100
|
offsetRef.current = offset + newItems.length;
|
|
90
101
|
} catch {
|
|
102
|
+
if (superseded()) return;
|
|
91
103
|
setHasMore(false);
|
|
92
104
|
} finally {
|
|
93
|
-
setIsLoading(false);
|
|
105
|
+
if (!superseded()) setIsLoading(false);
|
|
94
106
|
}
|
|
95
107
|
}, [client?.baseUrl, client?.apiPath, getAuthToken, pageSize]);
|
|
96
108
|
|
|
@@ -100,11 +112,23 @@ user };
|
|
|
100
112
|
}, [fetchUsers]);
|
|
101
113
|
|
|
102
114
|
const search = useCallback((searchStr: string) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
// Debounced like the relation picker's type-ahead: a request per
|
|
116
|
+
// keystroke is both wasteful and the thing that makes an out-of-order
|
|
117
|
+
// response likely in the first place.
|
|
118
|
+
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
|
119
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
120
|
+
setCurrentSearch(searchStr);
|
|
121
|
+
offsetRef.current = 0;
|
|
122
|
+
fetchUsers(searchStr, 0, false);
|
|
123
|
+
}, searchStr.trim() ? 300 : 0);
|
|
106
124
|
}, [fetchUsers]);
|
|
107
125
|
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
return () => {
|
|
128
|
+
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
|
129
|
+
};
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
108
132
|
const loadMore = useCallback(() => {
|
|
109
133
|
fetchUsers(currentSearch, offsetRef.current, true);
|
|
110
134
|
}, [fetchUsers, currentSearch]);
|
|
@@ -128,7 +152,14 @@ export interface UserSelectorProps {
|
|
|
128
152
|
value?: string | null;
|
|
129
153
|
onValueChange?: (userId: string | null) => void;
|
|
130
154
|
placeholder?: React.ReactNode;
|
|
131
|
-
|
|
155
|
+
/**
|
|
156
|
+
* `large` is the form's size and is on the shared 28/32/40/48 control
|
|
157
|
+
* scale, so a user picker lines up with the text field beside it.
|
|
158
|
+
*
|
|
159
|
+
* `small` and `medium` predate that scale (42 and 56) and are kept as they
|
|
160
|
+
* are because the table cells and the filter row are built around them.
|
|
161
|
+
*/
|
|
162
|
+
size?: "small" | "medium" | "large";
|
|
132
163
|
disabled?: boolean;
|
|
133
164
|
invisible?: boolean;
|
|
134
165
|
clearable?: boolean;
|
|
@@ -171,6 +202,7 @@ export const UserSelector = React.forwardRef<
|
|
|
171
202
|
|
|
172
203
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
173
204
|
const [searchString, setSearchString] = useState<string>("");
|
|
205
|
+
const contextPortalContainer = usePortalContainer();
|
|
174
206
|
|
|
175
207
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
176
208
|
const sentinelRef = useRef<HTMLDivElement>(null);
|
|
@@ -192,9 +224,19 @@ export const UserSelector = React.forwardRef<
|
|
|
192
224
|
const selectedUser: User | null = value ? getUser(value) ?? null : null;
|
|
193
225
|
|
|
194
226
|
// If the user isn't in the cache, try to find in available items
|
|
195
|
-
const
|
|
227
|
+
const listedUser: User | null = selectedUser
|
|
196
228
|
?? (value ? availableItems.find(i => i.uid === value)?.user ?? null : null);
|
|
197
229
|
|
|
230
|
+
// Neither the loaded page nor the cache holds it: ask for that uid
|
|
231
|
+
// specifically. The list is one page of ten, so any installation with
|
|
232
|
+
// more users than that rendered an assigned field as *empty* — and an
|
|
233
|
+
// editor who is shown "no value" sets one, overwriting an assignment
|
|
234
|
+
// the UI never admitted was there. Lookups are coalesced and cached
|
|
235
|
+
// per API base by `useResolvedUser`, so a table of these costs one
|
|
236
|
+
// request.
|
|
237
|
+
const fetchedUser = useResolvedUser(value && !listedUser ? value : undefined);
|
|
238
|
+
const resolvedUser: User | null = listedUser ?? fetchedUser;
|
|
239
|
+
|
|
198
240
|
// IntersectionObserver for infinite scroll
|
|
199
241
|
const sentinelCallbackRef = useCallback((node: HTMLDivElement | null) => {
|
|
200
242
|
if (observerRef.current) {
|
|
@@ -294,10 +336,10 @@ export const UserSelector = React.forwardRef<
|
|
|
294
336
|
};
|
|
295
337
|
}, [isPopoverOpen]);
|
|
296
338
|
|
|
297
|
-
useInjectStyles("UserSelector", " [cmdk-group] { max-height: 40vh; overflow-y: auto; } ");
|
|
298
|
-
|
|
299
339
|
const resolvedPlaceholder = placeholder || <EmptyValue className={"ml-2"}/>;
|
|
300
|
-
|
|
340
|
+
// See RelationSelector: a modal dialog cancels wheel events outside its
|
|
341
|
+
// own content, so a list portaled to the body opens unscrollable.
|
|
342
|
+
const portalContainer = (contextPortalContainer ?? (typeof document !== "undefined" ? document.body : undefined)) ?? undefined;
|
|
301
343
|
|
|
302
344
|
return (
|
|
303
345
|
<>
|
|
@@ -317,9 +359,13 @@ export const UserSelector = React.forwardRef<
|
|
|
317
359
|
className={cls(
|
|
318
360
|
{
|
|
319
361
|
"min-h-[42px] py-1 px-2": size === "small",
|
|
320
|
-
"min-h-[56px] py-2 px-4": size === "medium"
|
|
362
|
+
"min-h-[56px] py-2 px-4": size === "medium",
|
|
363
|
+
"min-h-[48px] py-1 px-4": size === "large"
|
|
321
364
|
},
|
|
322
|
-
|
|
365
|
+
// `rounded-lg` like every other field box —
|
|
366
|
+
// `rounded-md` made this the one control in a
|
|
367
|
+
// row with a different corner.
|
|
368
|
+
"w-full select-none rounded-lg text-sm relative flex items-center",
|
|
323
369
|
invisible ? fieldBackgroundInvisibleMixin : fieldBackgroundMixin,
|
|
324
370
|
disabled ? fieldBackgroundDisabledMixin : fieldBackgroundHoverMixin,
|
|
325
371
|
className
|
|
@@ -330,6 +376,14 @@ export const UserSelector = React.forwardRef<
|
|
|
330
376
|
<div className="flex flex-row items-center gap-1 truncate flex-1 min-w-0 mr-2">
|
|
331
377
|
<UserDisplay user={resolvedUser}/>
|
|
332
378
|
</div>
|
|
379
|
+
) : value ? (
|
|
380
|
+
// A uid we could not resolve — deleted, or
|
|
381
|
+
// hidden from this viewer. Show it raw: the
|
|
382
|
+
// placeholder here would claim the field is
|
|
383
|
+
// unset, which is the one thing it is not.
|
|
384
|
+
<span className="text-sm truncate text-text-primary dark:text-text-primary-dark">
|
|
385
|
+
{value}
|
|
386
|
+
</span>
|
|
333
387
|
) : (
|
|
334
388
|
<span className="text-sm text-text-secondary dark:text-text-secondary-dark">
|
|
335
389
|
{resolvedPlaceholder}
|
|
@@ -345,7 +399,7 @@ export const UserSelector = React.forwardRef<
|
|
|
345
399
|
</IconButton>
|
|
346
400
|
)}
|
|
347
401
|
<ChevronDownIcon
|
|
348
|
-
size={size === "
|
|
402
|
+
size={size === "small" ? iconSize.small : iconSize.medium}
|
|
349
403
|
className={cls("transition", isPopoverOpen ? "rotate-180" : "")}
|
|
350
404
|
/>
|
|
351
405
|
</div>
|
|
@@ -4,17 +4,48 @@ import { ChevronLeftIcon, ErrorBoundary, MenuIcon } from "@rebasepro/ui";
|
|
|
4
4
|
import { deepEqual as equal } from "fast-equals"
|
|
5
5
|
|
|
6
6
|
import { useLargeLayout, useAdminModeController, useTranslation } from "@rebasepro/app";
|
|
7
|
+
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
7
8
|
import { AppContext } from "./useApp";
|
|
8
9
|
|
|
9
10
|
export const DRAWER_WIDTH = 280;
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Namespaced by base path: two admins served from one origin are two different
|
|
14
|
+
* navigations, and a single key would have them overwrite each other's drawer.
|
|
15
|
+
*/
|
|
16
|
+
export function drawerOpenStorageKey(basePath: string | undefined) {
|
|
17
|
+
return `rebase-drawer-open:${basePath || "/"}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `null` when the user has never toggled the drawer, or storage is unavailable. */
|
|
21
|
+
function readStoredDrawerOpen(key: string): boolean | null {
|
|
22
|
+
if (typeof window === "undefined") return null;
|
|
23
|
+
try {
|
|
24
|
+
const stored = window.localStorage.getItem(key);
|
|
25
|
+
return stored === null ? null : stored === "true";
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// Storage can be blocked: private mode, sandboxed iframe, cookie policy.
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeStoredDrawerOpen(key: string, open: boolean) {
|
|
33
|
+
if (typeof window === "undefined") return;
|
|
34
|
+
try {
|
|
35
|
+
window.localStorage.setItem(key, String(open));
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Same as above: a drawer that forgets is better than a crash.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
11
41
|
/**
|
|
12
42
|
* @group Core
|
|
13
43
|
*/
|
|
14
44
|
export interface ScaffoldProps {
|
|
15
45
|
|
|
16
46
|
/**
|
|
17
|
-
*
|
|
47
|
+
* Expand the collapsed drawer while the pointer is over it. On by default —
|
|
48
|
+
* pass `false` to keep the rail a rail until the user clicks the toggle.
|
|
18
49
|
*/
|
|
19
50
|
autoOpenDrawer?: boolean;
|
|
20
51
|
|
|
@@ -22,8 +53,8 @@ export interface ScaffoldProps {
|
|
|
22
53
|
* Start with the drawer expanded rather than collapsed to icons.
|
|
23
54
|
*
|
|
24
55
|
* Distinct from {@link autoOpenDrawer}, which is a hover behaviour: this one
|
|
25
|
-
* only seeds the
|
|
26
|
-
*
|
|
56
|
+
* only seeds the very first visit. Once the user toggles the drawer, that
|
|
57
|
+
* choice is stored in `localStorage` and wins over this prop on every load.
|
|
27
58
|
*
|
|
28
59
|
* Ignored on small layouts, where an expanded drawer covers the content it is
|
|
29
60
|
* meant to navigate.
|
|
@@ -62,7 +93,7 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
62
93
|
|
|
63
94
|
const {
|
|
64
95
|
children,
|
|
65
|
-
autoOpenDrawer,
|
|
96
|
+
autoOpenDrawer = true,
|
|
66
97
|
defaultDrawerOpen = false,
|
|
67
98
|
logo,
|
|
68
99
|
className,
|
|
@@ -83,25 +114,96 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
|
|
|
83
114
|
const includeDrawer = drawerChildren.length > 0;
|
|
84
115
|
const largeLayout = useLargeLayout();
|
|
85
116
|
|
|
117
|
+
const storageKey = drawerOpenStorageKey(useUrlController().basePath);
|
|
118
|
+
|
|
86
119
|
// Seeded once, deliberately: a `useEffect` syncing this to the prop would
|
|
87
120
|
// re-expand the drawer under a user who had just collapsed it.
|
|
88
|
-
const [drawerOpen,
|
|
121
|
+
const [drawerOpen, setDrawerOpenState] = React.useState(defaultDrawerOpen && largeLayout);
|
|
89
122
|
const [onHover, setOnHover] = React.useState(false);
|
|
90
123
|
|
|
91
|
-
|
|
124
|
+
// The stored choice is a client-only fact, so it is applied after the first
|
|
125
|
+
// render rather than seeded into it: reading storage while rendering would
|
|
126
|
+
// make the client disagree with server-rendered HTML. A layout effect runs
|
|
127
|
+
// before paint, so the drawer still arrives in its remembered state.
|
|
128
|
+
// `defaultDrawerOpen` seeds the very first visit and is ignored after that.
|
|
129
|
+
React.useLayoutEffect(() => {
|
|
130
|
+
const stored = readStoredDrawerOpen(storageKey);
|
|
131
|
+
if (stored === null) return;
|
|
132
|
+
setDrawerOpenState(stored && largeLayout);
|
|
133
|
+
// Deliberately not re-run on `largeLayout`: crossing the breakpoint is
|
|
134
|
+
// the effect below, and re-running here would undo a user's toggle.
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
+
}, [storageKey]);
|
|
137
|
+
|
|
138
|
+
// One piece of state drives two different things: the expanded rail on
|
|
139
|
+
// large layouts, and the modal sheet on small ones. Carrying an expanded
|
|
140
|
+
// rail across the breakpoint would drop that sheet, overlay and all, over
|
|
141
|
+
// the content — so the crossing resets it, and widening again restores
|
|
142
|
+
// whatever the user last chose.
|
|
143
|
+
const wasLargeLayout = React.useRef(largeLayout);
|
|
144
|
+
React.useEffect(() => {
|
|
145
|
+
if (wasLargeLayout.current === largeLayout) return;
|
|
146
|
+
wasLargeLayout.current = largeLayout;
|
|
147
|
+
setDrawerOpenState(largeLayout ? (readStoredDrawerOpen(storageKey) ?? defaultDrawerOpen) : false);
|
|
148
|
+
}, [largeLayout, defaultDrawerOpen, storageKey]);
|
|
149
|
+
|
|
150
|
+
const setDrawerOpen = useCallback((open: boolean) => {
|
|
151
|
+
setDrawerOpenState(open);
|
|
152
|
+
// Small layouts render the drawer as a modal sheet. Remembering that
|
|
153
|
+
// one would greet the next load with an overlay over the content.
|
|
154
|
+
if (!largeLayout) return;
|
|
155
|
+
writeStoredDrawerOpen(storageKey, open);
|
|
156
|
+
}, [largeLayout, storageKey]);
|
|
157
|
+
|
|
158
|
+
// The pointer has left, but a popover the drawer opened is still up, so the
|
|
159
|
+
// collapse is owed rather than cancelled. See `setOnHoverFalse`.
|
|
160
|
+
const collapseWhenPopoverCloses = React.useRef(false);
|
|
161
|
+
|
|
162
|
+
const setOnHoverTrue = useCallback(() => {
|
|
163
|
+
// Hover expansion is the default; `autoOpenDrawer={false}` opts out for
|
|
164
|
+
// admins that want the rail to stay a rail until the toggle is clicked.
|
|
165
|
+
if (!autoOpenDrawer) return;
|
|
166
|
+
collapseWhenPopoverCloses.current = false;
|
|
167
|
+
setOnHover(true);
|
|
168
|
+
}, [autoOpenDrawer]);
|
|
92
169
|
const setOnHoverFalse = useCallback(() => {
|
|
93
|
-
// Don't collapse the drawer
|
|
94
|
-
|
|
170
|
+
// Don't collapse the drawer out from under an open popover/dropdown —
|
|
171
|
+
// its content is portalled outside the drawer, so reaching for it reads
|
|
172
|
+
// as a mouseleave. The collapse is owed until that popover closes.
|
|
173
|
+
if (document.querySelector("[data-radix-popper-content-wrapper]")) {
|
|
174
|
+
collapseWhenPopoverCloses.current = true;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
collapseWhenPopoverCloses.current = false;
|
|
95
178
|
setOnHover(false);
|
|
96
179
|
}, []);
|
|
97
180
|
|
|
181
|
+
// Nothing fires a second mouseleave when the popover finally closes, so the
|
|
182
|
+
// owed collapse has to be noticed rather than waited for. Only mounted
|
|
183
|
+
// while the drawer is actually floating open.
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
if (!onHover) return;
|
|
186
|
+
const collapseIfOwed = () => {
|
|
187
|
+
if (!collapseWhenPopoverCloses.current) return;
|
|
188
|
+
if (document.querySelector("[data-radix-popper-content-wrapper]")) return;
|
|
189
|
+
collapseWhenPopoverCloses.current = false;
|
|
190
|
+
setOnHover(false);
|
|
191
|
+
};
|
|
192
|
+
const observer = new MutationObserver(collapseIfOwed);
|
|
193
|
+
observer.observe(document.body, {
|
|
194
|
+
childList: true,
|
|
195
|
+
subtree: true
|
|
196
|
+
});
|
|
197
|
+
return () => observer.disconnect();
|
|
198
|
+
}, [onHover]);
|
|
199
|
+
|
|
98
200
|
const handleDrawerOpen = useCallback(() => {
|
|
99
201
|
setDrawerOpen(true);
|
|
100
|
-
}, []);
|
|
202
|
+
}, [setDrawerOpen]);
|
|
101
203
|
|
|
102
204
|
const handleDrawerClose = useCallback(() => {
|
|
103
205
|
setDrawerOpen(false);
|
|
104
|
-
}, []);
|
|
206
|
+
}, [setDrawerOpen]);
|
|
105
207
|
|
|
106
208
|
const computedDrawerOpen: boolean = drawerOpen;
|
|
107
209
|
const computedDrawerHovered = Boolean(largeLayout && onHover);
|
|
@@ -3,22 +3,17 @@ import React, { useEffect, useState, useCallback } from "react";
|
|
|
3
3
|
;
|
|
4
4
|
import { useApiBase, useApiConfig } from "@rebasepro/app";
|
|
5
5
|
import { useAuthController } from "@rebasepro/app";
|
|
6
|
+
import { formatRelativeTime } from "@rebasepro/utils";
|
|
6
7
|
import { HistoryEntryData } from "../../hooks";
|
|
7
8
|
import type { AdminCollection } from "@rebasepro/admin-types";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
const now = new Date();
|
|
11
|
-
const diffMs = now.getTime() - date.getTime();
|
|
12
|
-
const diffSeconds = Math.floor(diffMs / 1000);
|
|
13
|
-
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
14
|
-
const diffHours = Math.floor(diffMinutes / 60);
|
|
15
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
10
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return date.toLocaleDateString();
|
|
12
|
+
function getRelativeTimeString(date: Date): string {
|
|
13
|
+
// An edit is in the past, but the timestamp comes from the server and the
|
|
14
|
+
// phrase is rendered against the browser's clock — a few seconds of skew
|
|
15
|
+
// used to be enough to make a fresh edit read as the wrong thing entirely.
|
|
16
|
+
return formatRelativeTime(date, { maxMs: THIRTY_DAYS_MS }) ?? date.toLocaleDateString();
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
/**
|
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type { EntityViewBindingProps } from "./EntityViewBinding";
|
|
2
2
|
export { EntityViewBinding } from "./EntityViewBinding";
|
|
3
3
|
|
|
4
|
+
export type { EntityDisplayHeaderProps } from "./EntityDisplayHeader";
|
|
5
|
+
export { EntityDisplayHeader, HEADER_DISPLAY_ROLES } from "./EntityDisplayHeader";
|
|
6
|
+
|
|
4
7
|
export * from "./DetailViewBinding";
|
|
5
8
|
|
|
6
9
|
export type { SelectionProps } from "./ReferenceTable/SelectionTableBinding";
|
|
@@ -29,9 +29,26 @@ import {
|
|
|
29
29
|
Tooltip
|
|
30
30
|
} from "@rebasepro/ui";
|
|
31
31
|
import { downloadEntitiesExport } from "./export";
|
|
32
|
+
import { fetchAllEntitiesForExport, MAX_EXPORT_ROWS } from "./fetch_export_data";
|
|
32
33
|
|
|
33
34
|
const DOCS_LIMIT = 500;
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Additional-field builders run per row and may do I/O, so a `Promise.all` over
|
|
38
|
+
* the whole export is one request per row all at once. That was bounded only by
|
|
39
|
+
* the 50-row cap the export used to have; now that the read is paginated, it is
|
|
40
|
+
* bounded here instead.
|
|
41
|
+
*/
|
|
42
|
+
const ADDITIONAL_FIELDS_CONCURRENCY = 50;
|
|
43
|
+
|
|
44
|
+
async function mapInChunks<T, R>(items: T[], fn: (item: T) => Promise<R>, chunkSize = ADDITIONAL_FIELDS_CONCURRENCY): Promise<R[]> {
|
|
45
|
+
const results: R[] = [];
|
|
46
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
47
|
+
results.push(...await Promise.all(items.slice(i, i + chunkSize).map(fn)));
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
export function ExportCollectionAction<M extends Record<string, unknown>, USER extends User>({
|
|
36
53
|
collection,
|
|
37
54
|
path,
|
|
@@ -59,6 +76,11 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
59
76
|
const context = useAdminContext<USER>();
|
|
60
77
|
const dataClient = useData();
|
|
61
78
|
|
|
79
|
+
// Said before the download starts, not discovered halfway through it: the
|
|
80
|
+
// walk refuses rather than writing a short file, so the dialog has to name
|
|
81
|
+
// the ceiling it is about to hit.
|
|
82
|
+
const tooManyToExport = collectionEntitiesCount !== undefined && collectionEntitiesCount > MAX_EXPORT_ROWS;
|
|
83
|
+
|
|
62
84
|
const canExport = !exportAllowed || exportAllowed({
|
|
63
85
|
collectionEntitiesCount: collectionEntitiesCount ?? 0,
|
|
64
86
|
path,
|
|
@@ -67,6 +89,7 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
67
89
|
|
|
68
90
|
const [dataLoading, setDataLoading] = React.useState<boolean>(false);
|
|
69
91
|
const [dataLoadingError, setDataLoadingError] = React.useState<Error | undefined>();
|
|
92
|
+
const [progress, setProgress] = React.useState<{ loaded: number, total?: number }>({ loaded: 0 });
|
|
70
93
|
|
|
71
94
|
const [open, setOpen] = React.useState(false);
|
|
72
95
|
|
|
@@ -84,7 +107,7 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
84
107
|
const additionalFields = collection.additionalFields;
|
|
85
108
|
|
|
86
109
|
const resolvedExportColumnsValues: Record<string, any>[] = additionalExportFields
|
|
87
|
-
? await
|
|
110
|
+
? await mapInChunks(entities, async (entity) => {
|
|
88
111
|
return (await Promise.all(additionalExportFields.map(async (column) => {
|
|
89
112
|
return {
|
|
90
113
|
[column.key]: await column.builder({
|
|
@@ -94,11 +117,11 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
94
117
|
};
|
|
95
118
|
}))).reduce((a, b) => ({ ...a,
|
|
96
119
|
...b }), {});
|
|
97
|
-
})
|
|
120
|
+
})
|
|
98
121
|
: [];
|
|
99
122
|
|
|
100
123
|
const resolvedColumnsValues: Record<string, any>[] = additionalFields
|
|
101
|
-
? await
|
|
124
|
+
? await mapInChunks(entities, async (entity) => {
|
|
102
125
|
return (await Promise.all(additionalFields
|
|
103
126
|
.map(async (field) => {
|
|
104
127
|
if (!field.value)
|
|
@@ -111,64 +134,74 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
111
134
|
};
|
|
112
135
|
}))).reduce((a, b) => ({ ...a,
|
|
113
136
|
...b }), {});
|
|
114
|
-
})
|
|
137
|
+
})
|
|
115
138
|
: [];
|
|
116
139
|
return [...resolvedExportColumnsValues, ...resolvedColumnsValues];
|
|
117
140
|
}, [exportConfig?.additionalFields]);
|
|
118
141
|
|
|
119
142
|
const doDownload = useCallback(async (collection: AdminCollection<M>,
|
|
120
|
-
exportConfig: ExportConfig<any> | undefined) => {
|
|
143
|
+
exportConfig: ExportConfig<any> | undefined): Promise<boolean> => {
|
|
121
144
|
|
|
122
145
|
onAnalyticsEvent?.("export_collection", {
|
|
123
146
|
collection: collection.slug
|
|
124
147
|
});
|
|
125
148
|
setDataLoading(true);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
149
|
+
setDataLoadingError(undefined);
|
|
150
|
+
setProgress({ loaded: 0 });
|
|
151
|
+
try {
|
|
152
|
+
// Paginated, not `find({})`: an absent limit resolves to 50 rows
|
|
153
|
+
// server-side, so the export used to be the first page of the
|
|
154
|
+
// collection under a filename that read like all of it.
|
|
155
|
+
const data = await fetchAllEntitiesForExport<M>({
|
|
156
|
+
accessor: dataClient.collection(path) as { find: (params?: any) => Promise<any> },
|
|
157
|
+
onProgress: (loaded, total) => setProgress({ loaded,
|
|
158
|
+
total })
|
|
159
|
+
});
|
|
160
|
+
const additionalData = await fetchAdditionalFields(data);
|
|
161
|
+
const additionalHeaders = [
|
|
162
|
+
...exportConfig?.additionalFields?.map(column => column.key) ?? [],
|
|
163
|
+
...collection.additionalFields?.map(field => field.key) ?? []
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const defaultValues = includeUndefinedValues ? getDefaultValuesFor(collection.properties) : undefined;
|
|
167
|
+
const dataWithDefaults = defaultValues
|
|
168
|
+
? data.map(entity => ({
|
|
169
|
+
...entity,
|
|
170
|
+
values: { ...defaultValues,
|
|
142
171
|
...entity.values }
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
}))
|
|
173
|
+
: data;
|
|
174
|
+
downloadEntitiesExport({
|
|
175
|
+
data: dataWithDefaults,
|
|
176
|
+
additionalData,
|
|
177
|
+
properties: collection.properties,
|
|
178
|
+
propertiesOrder: collection.propertiesOrder,
|
|
179
|
+
name: collection.name,
|
|
180
|
+
flattenArrays,
|
|
181
|
+
additionalHeaders,
|
|
182
|
+
exportType,
|
|
183
|
+
dateExportType
|
|
184
|
+
});
|
|
185
|
+
onAnalyticsEvent?.("export_collection_success", {
|
|
186
|
+
collection: collection.slug
|
|
187
|
+
});
|
|
188
|
+
return true;
|
|
189
|
+
} catch (e) {
|
|
190
|
+
console.error("Error loading export data", e);
|
|
191
|
+
setDataLoadingError(e as Error);
|
|
192
|
+
return false;
|
|
193
|
+
} finally {
|
|
194
|
+
setDataLoading(false);
|
|
195
|
+
}
|
|
166
196
|
|
|
167
197
|
}, [onAnalyticsEvent, dataClient, path, fetchAdditionalFields, includeUndefinedValues, flattenArrays, exportType, dateExportType]);
|
|
168
198
|
|
|
169
199
|
const onOkClicked = useCallback(() => {
|
|
170
|
-
|
|
171
|
-
|
|
200
|
+
// The dialog stays open until the walk finishes: it is the only place a
|
|
201
|
+
// multi-page export reports progress, or reports that it failed.
|
|
202
|
+
doDownload(collection, exportConfig).then((downloaded) => {
|
|
203
|
+
if (downloaded) handleClose();
|
|
204
|
+
});
|
|
172
205
|
}, [doDownload, collection, exportConfig, handleClose]);
|
|
173
206
|
|
|
174
207
|
return <>
|
|
@@ -194,13 +227,23 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
194
227
|
|
|
195
228
|
<div>{t("download_table_csv")}</div>
|
|
196
229
|
|
|
197
|
-
{collectionEntitiesCount !== undefined && collectionEntitiesCount > DOCS_LIMIT &&
|
|
230
|
+
{collectionEntitiesCount !== undefined && collectionEntitiesCount > DOCS_LIMIT && !tooManyToExport &&
|
|
198
231
|
<Alert color={"warning"}>
|
|
199
232
|
<div>
|
|
200
233
|
{t("large_number_of_documents", { count: collectionEntitiesCount.toString() })}
|
|
201
234
|
</div>
|
|
202
235
|
</Alert>}
|
|
203
236
|
|
|
237
|
+
{tooManyToExport &&
|
|
238
|
+
<Alert color={"error"}>
|
|
239
|
+
<div>
|
|
240
|
+
{t("too_many_documents_to_export", {
|
|
241
|
+
count: (collectionEntitiesCount ?? 0).toString(),
|
|
242
|
+
limit: MAX_EXPORT_ROWS.toString()
|
|
243
|
+
})}
|
|
244
|
+
</div>
|
|
245
|
+
</Alert>}
|
|
246
|
+
|
|
204
247
|
<div className={"flex flex-row gap-4"}>
|
|
205
248
|
<div className={"p-4 flex flex-col"}>
|
|
206
249
|
<RadioGroup value={exportType} onValueChange={(v) => setExportType(v as "csv" | "json")}>
|
|
@@ -242,6 +285,10 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
242
285
|
onValueChange={setIncludeUndefinedValues}
|
|
243
286
|
label={t("include_undefined_values")}/>
|
|
244
287
|
|
|
288
|
+
{dataLoadingError && <Alert color={"error"}>
|
|
289
|
+
<div>{dataLoadingError.message}</div>
|
|
290
|
+
</Alert>}
|
|
291
|
+
|
|
245
292
|
{!canExport && notAllowedView}
|
|
246
293
|
|
|
247
294
|
</DialogContent>
|
|
@@ -250,13 +297,19 @@ export function ExportCollectionAction<M extends Record<string, unknown>, USER e
|
|
|
250
297
|
|
|
251
298
|
{dataLoading && <CircularProgress size={"smallest"}/>}
|
|
252
299
|
|
|
300
|
+
{dataLoading && <Label>
|
|
301
|
+
{progress.total !== undefined
|
|
302
|
+
? `${progress.loaded} / ${progress.total}`
|
|
303
|
+
: `${progress.loaded}`}
|
|
304
|
+
</Label>}
|
|
305
|
+
|
|
253
306
|
<Button onClick={handleClose}
|
|
254
307
|
variant={"text"}>
|
|
255
308
|
{t("cancel")}
|
|
256
309
|
</Button>
|
|
257
310
|
|
|
258
311
|
<Button onClick={onOkClicked}
|
|
259
|
-
disabled={dataLoading || !canExport}>
|
|
312
|
+
disabled={dataLoading || !canExport || tooManyToExport}>
|
|
260
313
|
{t("download")}
|
|
261
314
|
</Button>
|
|
262
315
|
|