@rebasepro/admin 0.13.1-canary.gef9608c → 0.14.0
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-7b-kSLno.js → CollectionEditorDialog-CSheud_E.js} +19 -9
- package/dist/CollectionEditorDialog-CSheud_E.js.map +1 -0
- package/dist/{PropertyEditView-C-31uvpG.js → PropertyEditView-CWZlC1gq.js} +5 -5
- package/dist/PropertyEditView-CWZlC1gq.js.map +1 -0
- package/dist/{RouterCollectionsStudioView-BJylABq3.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/ClearFilterSortButton.d.ts +7 -1
- 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 +26 -0
- package/dist/components/CollectionViewBinding/SortButton.d.ts +7 -1
- package/dist/components/CollectionViewBinding/SplitListCloseButton.d.ts +17 -6
- package/dist/components/CollectionViewBinding/SplitListShowButton.d.ts +1 -1
- package/dist/components/CollectionViewBinding/usePreviewSlots.d.ts +26 -1
- package/dist/components/DetailViewBinding.d.ts +7 -0
- package/dist/components/DrawerNavigationGroup.d.ts +4 -2
- package/dist/components/DrawerNavigationItem.d.ts +13 -5
- package/dist/components/EditViewBinding.d.ts +22 -7
- package/dist/components/EntityIdentityBar.d.ts +24 -1
- package/dist/components/RelationSelector.d.ts +13 -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-C-XG3aRh.js → export-L53WektO.js} +175 -39
- package/dist/export-L53WektO.js.map +1 -0
- package/dist/form/form_utils.d.ts +38 -1
- package/dist/{history-DfMuvPfr.js → history-D5SKygpJ.js} +2 -2
- package/dist/{history-DfMuvPfr.js.map → history-D5SKygpJ.js.map} +1 -1
- package/dist/{import-B0GyllB0.js → import-C_4edkoD.js} +5 -4
- package/dist/import-C_4edkoD.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +96 -80
- package/dist/index.js.map +1 -1
- package/dist/preview/components/date_fns_locales.d.ts +9 -0
- package/dist/util/view_constants.d.ts +24 -0
- package/dist/util/view_mode.d.ts +18 -0
- package/dist/{util-DHNXodBo.js → util-C-D5yD2n.js} +2173 -673
- 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/ClearFilterSortButton.tsx +16 -7
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +2 -2
- 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 +18 -1
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +13 -7
- package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +2 -1
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +816 -335
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +1 -1
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +26 -6
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +9 -0
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +45 -10
- package/src/components/CollectionViewBinding/SearchExplanation.tsx +106 -0
- package/src/components/CollectionViewBinding/SearchHighlight.tsx +311 -0
- package/src/components/CollectionViewBinding/SlotValue.tsx +96 -0
- package/src/components/CollectionViewBinding/SortButton.tsx +16 -4
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +23 -9
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +3 -3
- package/src/components/CollectionViewBinding/SplitListView.tsx +36 -9
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +10 -5
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +11 -3
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +76 -24
- package/src/components/CollectionViewBinding/utils.ts +7 -4
- package/src/components/DefaultDrawer.tsx +7 -0
- package/src/components/DetailViewBinding.tsx +33 -9
- package/src/components/DrawerNavigationGroup.tsx +12 -32
- package/src/components/DrawerNavigationItem.tsx +33 -12
- package/src/components/EditViewBinding.tsx +175 -28
- package/src/components/EntityIdentityBar.tsx +109 -17
- package/src/components/EntityInspector.tsx +24 -6
- package/src/components/EntityViewBinding.tsx +4 -2
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +4 -2
- package/src/components/RelationSelector.tsx +61 -11
- package/src/components/SearchIconsView.tsx +14 -33
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -0
- package/src/components/SidePanelBinding.tsx +36 -25
- package/src/components/UserSelector.tsx +52 -9
- package/src/components/history/LastEditedByIndicator.tsx +7 -12
- 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 +12 -3
- package/src/form/EntityFormBinding.tsx +8 -12
- package/src/form/PropertyFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -2
- package/src/form/field_bindings/BinaryFieldBinding.tsx +1 -0
- package/src/form/field_bindings/BlockFieldBinding.tsx +3 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -0
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +3 -0
- package/src/form/field_bindings/MapFieldBinding.tsx +2 -2
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +15 -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/VectorFieldBinding.tsx +1 -0
- package/src/form/form_utils.ts +78 -0
- package/src/index.ts +9 -1
- package/src/preview/PropertyPreview.tsx +7 -3
- 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/date_fns_locales.ts +125 -0
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +6 -3
- package/src/util/navigation_utils.ts +4 -2
- 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-7b-kSLno.js.map +0 -1
- package/dist/PropertyEditView-C-31uvpG.js.map +0 -1
- package/dist/RouterCollectionsStudioView-BJylABq3.js.map +0 -1
- package/dist/export-C-XG3aRh.js.map +0 -1
- package/dist/import-B0GyllB0.js.map +0 -1
- package/dist/util-DHNXodBo.js.map +0 -1
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import * as React$1 from "react";
|
|
2
2
|
import React, { Suspense, createContext, createElement, forwardRef, lazy, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { deepEqual } from "fast-equals";
|
|
4
|
-
import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightToLineIcon, ArrowUpDownIcon, ArrowUpIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, CHIP_SEED_KEYS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon,
|
|
5
|
-
import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getSubcollections, getTableName, isPropertyBuilder, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
|
|
6
|
-
import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, EntityDisplayCache, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, entityDisplayKey, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, iconsSearch, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
|
|
4
|
+
import { Alert, AlertCircleIcon, AlertTriangleIcon, AlignLeftIcon, ArrowDownIcon, ArrowLeftIcon, ArrowRightToLineIcon, ArrowUpDownIcon, ArrowUpIcon, Badge, BooleanSwitchWithLabel, Button, CHIP_COLORS, CHIP_SEED_KEYS, CalendarIcon, Card, CardView, CenteredView, CheckCircleIcon, CheckIcon, Checkbox, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, ChevronsUpDownIcon, Chip, CircleIcon, CircularProgress, CircularProgressCenter, CodeIcon, Collapse, ColumnsIcon, CopyIcon, DateTimeField, Dialog, DialogActions, DialogContent, DialogTitle, ErrorBoundary, ExpandablePanel, ExternalLinkIcon, EyeIcon, EyeOffIcon, FileTextIcon, FileUpload, FilterChip, FilterIcon, FilterXIcon, FlagIcon, FolderUpIcon, FunctionSquareIcon, GlobeIcon, HandleIcon, HashIcon, HistoryIcon, IconButton, ImageOffIcon, KanbanIcon, KanbanView, KeyRoundIcon, Label, LayoutGridIcon, Link2Icon, LinkIcon, ListIcon, ListOrderedIcon, ListView, LoadingButton, LucideIconByName, MailIcon, Markdown, Maximize2Icon, Menu, MenuItem, MinusIcon, MoreVerticalIcon, MultiSelect, MultiSelectItem, PanelLeftCloseIcon, PanelLeftOpenIcon, PencilIcon, PlusIcon, Popover, PopoverPrimitive, Portal, QuoteIcon, RadioGroup, RadioGroupItem, RefreshCwIcon, RepeatIcon, ResizablePanels, Rows3Icon, SearchBar, SearchIcon, Select, SelectItem, Separator, SettingsIcon, Sheet, Skeleton, Tab, Table, TableBody, TableCell, TableHeader, TableIcon, TableRow, Tabs, TextField, TextIcon, TextareaAutosize, ToggleButtonGroup, Tooltip, Trash2Icon, Typography, Unlink2Icon, UploadIcon, UserIcon, VirtualTable, VirtualTableDateField, VirtualTableInput, VirtualTableInput as VirtualTableInput$1, VirtualTableNumberInput, VirtualTableSwitch, VoteIcon, XCircleIcon, XIcon, cls, coolIconKeys, createVirtualTableSelectionStore, debounce, defaultBorderMixin, fieldBackgroundDisabledMixin, fieldBackgroundHoverMixin, fieldBackgroundInvisibleMixin, fieldBackgroundMixin, focusedDisabled, getColorSchemeForSeed, iconSize, paperMixin, useDebouncedCallback, useOutsideAlerter, usePortalContainer } from "@rebasepro/ui";
|
|
5
|
+
import { CollectionRegistry, DEFAULT_ONE_OF_TYPE, DEFAULT_ONE_OF_VALUE, canReadCollection, enumToObjectEntries, getArrayResolvedProperties, getDefaultValueFor, getDefaultValueFortype, getDefaultValuesFor, getEntityChildViews, getLabelOrConfigFrom, getReferenceFrom, getRelationFrom, getRelationTargetPath, getSubcollections, getTableName, isPropertyBuilder, normalizeToEntityRelation, resolveArrayProperties, resolveEnumValues, resolveProperty, resolveRelationProperty, resolveStorageFilenameString, resolveStoragePathString, resolveStorageSource } from "@rebasepro/common";
|
|
6
|
+
import { CONTAINER_FULL_WIDTH, CollectionScopeProvider, DEFAULT_PAGE_SIZE, EntityDisplayCache, ErrorTooltip, ErrorView, IconForView, PluginProviderStack, SIDE_PANEL_DEFAULT_WIDTH, StorageSourceContext, UnsavedChangesDialog, UserDisplay, addInitialSlash, apiBaseOf, buildEnumLabel, createFormexStub, deleteEntityWithCallbacks, entityDisplayKey, flattenKeys, getAdminEntityChildViews, getCollectionBySlugWithin, getCollectionPathsCombinations, getColorScheme, getColumnKeysForProperty, getDisplayPropertyKey, getDisplayResolver, getEntityFromCache, getEntityFromMemoryCache, getEntityImagePreviewPropertyKey, getEntityPreviewKeys, getFormFieldKeys, getIcon, getLastSegment as getLastSegment$1, getLeadingRelationTitleKey, getLocalChangesBackup, getNavigationEntriesFromPath, getParentReferencesFromPath, getPropertyInPath, getRedundantChildViewColumnIds, getRelationIncludeParams, getRowHeight, getTitlePropertyCandidates, getTitlePropertyKey, getTitlePropertyKeyForValues, hasDeclaredDisplay, iconsSearch, isDisabled, isFilterableRelation, isHidden, isReadOnly, looksLikeIdentifierValue, removeEntityFromCache, removeEntityFromMemoryCache, removeInitialAndTrailingSlashes, removeInitialAndTrailingSlashes as removeInitialAndTrailingSlashes$1, removeInitialSlash, removeTrailingSlash, removeTrailingSlash as removeTrailingSlash$1, resolveCollectionPathIds, resolveCollectionPathIds as resolveCollectionPathIds$1, resolveComponentRef, resolveDefaultSelectedView, resolveFilterOperators, resolveFormLayout, saveEntityToCache, saveEntityToMemoryCache, saveEntityWithCallbacks, useAnalyticsController, useApiBase, useApiConfig, useAuthController, useCollectionScope, useColumnIds, useComponentOverride, useCustomizationController, useData, useDataTableController, useFetch, useLargeLayout, useModeController, useNavigationBlocker, usePermissions, useRebaseClient, useRebaseContext, useRelationSelector, useResolvedComponent, useScrollRestoration, useSlot, useSnackbarController, useStorageSource, useStorageSources, useStorageUploadController, useTranslation, useUnsavedChangesDialog, useUserConfigurationPersistence } from "@rebasepro/app";
|
|
7
7
|
import { Field, Formex, getIn, setIn, useCreateFormex, useFormex } from "@rebasepro/forms";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { defaultDateFormat, getHashValue, getValueInPath, hydrateRegExp, isObject, mergeDeep, randomString, toArray } from "@rebasepro/utils";
|
|
9
|
+
import { defaultDateFormat, formatRelativeTime, getHashValue, getValueInPath, hydrateRegExp, isArrayValue, isObject, isPrototypePollutingKey, mergeDeep, pathTraversesPrototype, randomString, readStoredJson, toArray, writeStoredJson } from "@rebasepro/utils";
|
|
10
10
|
import { resolveAdminCollection } from "@rebasepro/admin-types";
|
|
11
11
|
import { format } from "date-fns";
|
|
12
|
-
import * as locales from "date-fns/locale";
|
|
13
12
|
import { Command } from "cmdk";
|
|
14
13
|
import { useDropzone } from "react-dropzone";
|
|
15
|
-
import { EntityReference, EntityRelation, GeoPoint, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig } from "@rebasepro/types";
|
|
14
|
+
import { EntityReference, EntityRelation, GeoPoint, MAX_LIST_LIMIT, Vector, getCollectionDataPath, getDataSourceCapabilities, isRelationalCollectionConfig } from "@rebasepro/types";
|
|
16
15
|
import { z } from "zod";
|
|
17
16
|
import useMeasure from "react-use-measure";
|
|
18
17
|
import { Link, useLocation, useNavigate, useSearchParams } from "react-router";
|
|
@@ -20,7 +19,6 @@ import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, us
|
|
|
20
19
|
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
21
20
|
import { SortableContext, horizontalListSortingStrategy, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
|
22
21
|
import { CSS } from "@dnd-kit/utilities";
|
|
23
|
-
import ExcelJS from "exceljs";
|
|
24
22
|
import { inferTypeFromValue } from "@rebasepro/inference";
|
|
25
23
|
import { Highlight, themes } from "prism-react-renderer";
|
|
26
24
|
import { generateKeyBetween, generateNKeysBetween } from "fractional-indexing";
|
|
@@ -111,7 +109,7 @@ function getResolvedPropertyInPath(properties, path) {
|
|
|
111
109
|
if (path.includes(".")) {
|
|
112
110
|
const pathSegments = path.split(".");
|
|
113
111
|
const childProperty = properties[pathSegments[0]];
|
|
114
|
-
if (childProperty
|
|
112
|
+
if (childProperty?.type === "map" && childProperty.properties) return getResolvedPropertyInPath(childProperty.properties, pathSegments.slice(1).join("."));
|
|
115
113
|
}
|
|
116
114
|
}
|
|
117
115
|
}
|
|
@@ -1524,7 +1522,8 @@ function resolveCollectionSlotKeys(collection, authController, propertyConfigs)
|
|
|
1524
1522
|
subtitleKey: getDisplayPropertyKey(collection, "subtitle") ?? (sortedPreviewKeys.length > 0 ? sortedPreviewKeys[0] : void 0),
|
|
1525
1523
|
relationKeys,
|
|
1526
1524
|
statusKey,
|
|
1527
|
-
dateKey
|
|
1525
|
+
dateKey,
|
|
1526
|
+
tagsKey: getDisplayPropertyKey(collection, "tags")
|
|
1528
1527
|
};
|
|
1529
1528
|
}
|
|
1530
1529
|
/**
|
|
@@ -1541,6 +1540,7 @@ function resolveImageSlot(collection, imageKey, entity) {
|
|
|
1541
1540
|
const resolvedValue = ofProp ? (rawValue ?? []).length > 0 ? rawValue[0] : void 0 : rawValue;
|
|
1542
1541
|
if (resolvedValue === void 0 || resolvedValue === null) return void 0;
|
|
1543
1542
|
return {
|
|
1543
|
+
role: "image",
|
|
1544
1544
|
property: resolvedProperty,
|
|
1545
1545
|
propertyKey: imageKey,
|
|
1546
1546
|
value: resolvedValue
|
|
@@ -1569,14 +1569,8 @@ function formatDateValue$1(value) {
|
|
|
1569
1569
|
const date = value instanceof Date ? value : new Date(value);
|
|
1570
1570
|
if (isNaN(date.getTime())) return String(value);
|
|
1571
1571
|
const now = /* @__PURE__ */ new Date();
|
|
1572
|
-
const
|
|
1573
|
-
|
|
1574
|
-
const diffHours = Math.floor(diffMs / 36e5);
|
|
1575
|
-
const diffDays = Math.floor(diffMs / 864e5);
|
|
1576
|
-
if (diffMins < 1) return "Just now";
|
|
1577
|
-
if (diffMins < 60) return `${diffMins}m ago`;
|
|
1578
|
-
if (diffHours < 24) return `${diffHours}h ago`;
|
|
1579
|
-
if (diffDays < 7) return `${diffDays}d ago`;
|
|
1572
|
+
const relative = formatRelativeTime(date, { now });
|
|
1573
|
+
if (relative) return relative;
|
|
1580
1574
|
return date.toLocaleDateString(void 0, {
|
|
1581
1575
|
month: "short",
|
|
1582
1576
|
day: "numeric",
|
|
@@ -1603,7 +1597,7 @@ function useCollectionSlotKeys(collection, authController, propertyConfigs) {
|
|
|
1603
1597
|
* `React.memo` render functions or loops without violating hook rules.
|
|
1604
1598
|
*/
|
|
1605
1599
|
function resolveEntitySlots(entity, collection, slotKeys) {
|
|
1606
|
-
const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey } = slotKeys;
|
|
1600
|
+
const { titleKey, titleKeyCandidates, imageKey, subtitleKey, relationKeys, statusKey, dateKey, tagsKey } = slotKeys;
|
|
1607
1601
|
const image = imageKey ? resolveImageSlot(collection, imageKey, entity) : void 0;
|
|
1608
1602
|
const resolvedTitleKey = resolveEntityTitleKey(entity, titleKeyCandidates ?? (titleKey ? [titleKey] : []));
|
|
1609
1603
|
let title;
|
|
@@ -1611,6 +1605,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
|
|
|
1611
1605
|
const prop = collection.properties[resolvedTitleKey];
|
|
1612
1606
|
const val = getValueInPath(entity.values, resolvedTitleKey);
|
|
1613
1607
|
if (prop) title = {
|
|
1608
|
+
role: "title",
|
|
1614
1609
|
property: prop,
|
|
1615
1610
|
propertyKey: resolvedTitleKey,
|
|
1616
1611
|
value: val
|
|
@@ -1621,6 +1616,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
|
|
|
1621
1616
|
const prop = collection.properties[subtitleKey];
|
|
1622
1617
|
const val = getValueInPath(entity.values, subtitleKey);
|
|
1623
1618
|
if (prop && val !== void 0 && val !== null && val !== "") subtitle = {
|
|
1619
|
+
role: "subtitle",
|
|
1624
1620
|
property: prop,
|
|
1625
1621
|
propertyKey: subtitleKey,
|
|
1626
1622
|
value: val
|
|
@@ -1678,6 +1674,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
|
|
|
1678
1674
|
const prop = collection.properties[statusKey];
|
|
1679
1675
|
const val = getValueInPath(entity.values, statusKey);
|
|
1680
1676
|
if (prop && val !== void 0 && val !== null) status = {
|
|
1677
|
+
role: "status",
|
|
1681
1678
|
property: prop,
|
|
1682
1679
|
propertyKey: statusKey,
|
|
1683
1680
|
value: val
|
|
@@ -1688,12 +1685,25 @@ function resolveEntitySlots(entity, collection, slotKeys) {
|
|
|
1688
1685
|
const prop = collection.properties[dateKey];
|
|
1689
1686
|
const val = getValueInPath(entity.values, dateKey);
|
|
1690
1687
|
if (prop && val !== void 0 && val !== null) date = {
|
|
1688
|
+
role: "date",
|
|
1691
1689
|
property: prop,
|
|
1692
1690
|
propertyKey: dateKey,
|
|
1693
1691
|
value: val,
|
|
1694
1692
|
formatted: formatDateValue$1(val)
|
|
1695
1693
|
};
|
|
1696
1694
|
}
|
|
1695
|
+
let tags;
|
|
1696
|
+
if (tagsKey) {
|
|
1697
|
+
const prop = collection.properties[tagsKey];
|
|
1698
|
+
const val = getValueInPath(entity.values, tagsKey);
|
|
1699
|
+
const empty = val === void 0 || val === null || val === "" || Array.isArray(val) && val.length === 0;
|
|
1700
|
+
if (prop && !empty) tags = {
|
|
1701
|
+
role: "tags",
|
|
1702
|
+
property: prop,
|
|
1703
|
+
propertyKey: tagsKey,
|
|
1704
|
+
value: val
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1697
1707
|
return {
|
|
1698
1708
|
image,
|
|
1699
1709
|
title,
|
|
@@ -1701,6 +1711,7 @@ function resolveEntitySlots(entity, collection, slotKeys) {
|
|
|
1701
1711
|
relations,
|
|
1702
1712
|
status,
|
|
1703
1713
|
date,
|
|
1714
|
+
tags,
|
|
1704
1715
|
entityId: entity.id
|
|
1705
1716
|
};
|
|
1706
1717
|
}
|
|
@@ -1775,19 +1786,24 @@ function useEntitySlots(entity, collection, slotKeys) {
|
|
|
1775
1786
|
const image = useEntityDisplay("image", params);
|
|
1776
1787
|
const status = useEntityDisplay("status", params);
|
|
1777
1788
|
const date = useEntityDisplay("date", params);
|
|
1789
|
+
const tags = useEntityDisplay("tags", params);
|
|
1778
1790
|
return useMemo(() => {
|
|
1779
|
-
const computed = (resolved) => resolved.value !== void 0 && resolved.source === void 0 ? {
|
|
1780
|
-
|
|
1791
|
+
const computed = (role, resolved) => resolved.value !== void 0 && resolved.source === void 0 ? {
|
|
1792
|
+
role,
|
|
1793
|
+
value: resolved.value
|
|
1794
|
+
} : void 0;
|
|
1795
|
+
const computedDate = computed("date", date);
|
|
1781
1796
|
return {
|
|
1782
1797
|
...derived,
|
|
1783
|
-
title: computed(title) ?? derived.title,
|
|
1784
|
-
subtitle: computed(subtitle) ?? derived.subtitle,
|
|
1785
|
-
image: computed(image) ?? derived.image,
|
|
1786
|
-
status: computed(status) ?? derived.status,
|
|
1798
|
+
title: computed("title", title) ?? derived.title,
|
|
1799
|
+
subtitle: computed("subtitle", subtitle) ?? derived.subtitle,
|
|
1800
|
+
image: computed("image", image) ?? derived.image,
|
|
1801
|
+
status: computed("status", status) ?? derived.status,
|
|
1787
1802
|
date: computedDate ? {
|
|
1788
1803
|
...computedDate,
|
|
1789
1804
|
formatted: formatDateValue$1(computedDate.value)
|
|
1790
|
-
} : derived.date
|
|
1805
|
+
} : derived.date,
|
|
1806
|
+
tags: computed("tags", tags) ?? derived.tags
|
|
1791
1807
|
};
|
|
1792
1808
|
}, [
|
|
1793
1809
|
derived,
|
|
@@ -1795,7 +1811,8 @@ function useEntitySlots(entity, collection, slotKeys) {
|
|
|
1795
1811
|
subtitle,
|
|
1796
1812
|
image,
|
|
1797
1813
|
status,
|
|
1798
|
-
date
|
|
1814
|
+
date,
|
|
1815
|
+
tags
|
|
1799
1816
|
]);
|
|
1800
1817
|
}
|
|
1801
1818
|
//#endregion
|
|
@@ -2225,8 +2242,7 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
|
|
|
2225
2242
|
collection,
|
|
2226
2243
|
useCache: true
|
|
2227
2244
|
});
|
|
2228
|
-
|
|
2229
|
-
const usedEntity = entity ?? referencesCache.get(reference.pathWithId);
|
|
2245
|
+
const usedEntity = entity;
|
|
2230
2246
|
let body;
|
|
2231
2247
|
if (!reference) body = /* @__PURE__ */ jsx(ErrorView, { error: "Reference not set" });
|
|
2232
2248
|
else if (usedEntity && !usedEntity.values) body = /* @__PURE__ */ jsx(ErrorView, {
|
|
@@ -2285,7 +2301,6 @@ function ReferencePreviewExisting({ reference, collection, previewProperties, si
|
|
|
2285
2301
|
hover
|
|
2286
2302
|
});
|
|
2287
2303
|
}
|
|
2288
|
-
var referencesCache = /* @__PURE__ */ new Map();
|
|
2289
2304
|
//#endregion
|
|
2290
2305
|
//#region src/preview/components/InlineEntityListPreview.tsx
|
|
2291
2306
|
/**
|
|
@@ -2468,8 +2483,7 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
|
|
|
2468
2483
|
collection,
|
|
2469
2484
|
useCache: true
|
|
2470
2485
|
});
|
|
2471
|
-
|
|
2472
|
-
const usedEntity = passedEntity ?? entity ?? relationsCache.get(relation.pathWithId);
|
|
2486
|
+
const usedEntity = passedEntity ?? entity;
|
|
2473
2487
|
let body;
|
|
2474
2488
|
if (!relation) body = /* @__PURE__ */ jsx(ErrorView, { error: "Relation not set" });
|
|
2475
2489
|
else if (usedEntity && !usedEntity.values) body = /* @__PURE__ */ jsx(ErrorView, {
|
|
@@ -2528,7 +2542,6 @@ function RelationPreviewExisting({ relation, collection, previewProperties, size
|
|
|
2528
2542
|
hover
|
|
2529
2543
|
});
|
|
2530
2544
|
}
|
|
2531
|
-
var relationsCache = /* @__PURE__ */ new Map();
|
|
2532
2545
|
//#endregion
|
|
2533
2546
|
//#region src/preview/property_previews/ArrayOfRelationsPreview.tsx
|
|
2534
2547
|
/**
|
|
@@ -2539,8 +2552,9 @@ function ArrayOfRelationsPreview({ propertyKey, value, property, size, textOnly
|
|
|
2539
2552
|
if (Array.isArray(property?.of)) throw Error("Using array properties instead of single one in `of` in ArrayProperty");
|
|
2540
2553
|
if (property?.type !== "array" || !property.of || property.of.type !== "relation") throw Error("Picked wrong preview component ArrayOfRelationsPreview");
|
|
2541
2554
|
const ofProperty = property.of;
|
|
2555
|
+
const relationTargetPath = getRelationTargetPath(ofProperty);
|
|
2542
2556
|
if (nested || textOnly) return /* @__PURE__ */ jsx(InlineEntityListPreview, {
|
|
2543
|
-
items: (value ?? []).map((relation) => normalizeToEntityRelation(relation)).filter((relation) => Boolean(relation)),
|
|
2557
|
+
items: (value ?? []).map((relation) => normalizeToEntityRelation(relation, "relation", relationTargetPath)).filter((relation) => Boolean(relation)),
|
|
2544
2558
|
renderItem: (relation, index) => /* @__PURE__ */ jsx(RelationPreview, {
|
|
2545
2559
|
disabled: !ofProperty.relation,
|
|
2546
2560
|
previewProperties: ofProperty.admin?.previewProperties,
|
|
@@ -2554,7 +2568,7 @@ function ArrayOfRelationsPreview({ propertyKey, value, property, size, textOnly
|
|
|
2554
2568
|
return /* @__PURE__ */ jsx("div", {
|
|
2555
2569
|
className: "flex flex-col w-full gap-0.5",
|
|
2556
2570
|
children: value ? value.map((relation, index) => {
|
|
2557
|
-
const entityRelation = normalizeToEntityRelation(relation);
|
|
2571
|
+
const entityRelation = normalizeToEntityRelation(relation, "relation", relationTargetPath);
|
|
2558
2572
|
if (!entityRelation) return null;
|
|
2559
2573
|
return /* @__PURE__ */ jsx("div", {
|
|
2560
2574
|
className: "w-full",
|
|
@@ -2831,13 +2845,144 @@ function KeyValuePreview({ value }) {
|
|
|
2831
2845
|
});
|
|
2832
2846
|
}
|
|
2833
2847
|
//#endregion
|
|
2848
|
+
//#region src/preview/components/date_fns_locales.ts
|
|
2849
|
+
/**
|
|
2850
|
+
* One date-fns locale, fetched when the admin is actually configured to use it.
|
|
2851
|
+
*
|
|
2852
|
+
* `DatePreview` used to open with:
|
|
2853
|
+
*
|
|
2854
|
+
* import * as locales from "date-fns/locale";
|
|
2855
|
+
*
|
|
2856
|
+
* and then index that namespace with the configured locale. A namespace import
|
|
2857
|
+
* of a barrel is a reference to every member of it, so all ~77 locales were
|
|
2858
|
+
* bundled — 640 kB, in the entry chunk's static graph, to format dates in the
|
|
2859
|
+
* one locale a given deployment has configured (and `undefined`, meaning
|
|
2860
|
+
* en-US, for nearly all of them).
|
|
2861
|
+
*
|
|
2862
|
+
* A map of thunks is written out rather than derived from the locale key,
|
|
2863
|
+
* because `import(`date-fns/locale/${name}`)` is not statically analysable:
|
|
2864
|
+
* the bundler cannot see which modules it must emit, and the import fails at
|
|
2865
|
+
* runtime in a build. Each entry below is its own async chunk, and exactly one
|
|
2866
|
+
* of them is ever fetched.
|
|
2867
|
+
*
|
|
2868
|
+
* `fil` is in the `Locale` union and has no date-fns locale to import; it
|
|
2869
|
+
* resolved to `undefined` through the namespace too, so it still does.
|
|
2870
|
+
*/
|
|
2871
|
+
var dateFnsLocaleLoaders = {
|
|
2872
|
+
af: () => import("date-fns/locale/af").then((m) => m.af),
|
|
2873
|
+
ar: () => import("date-fns/locale/ar").then((m) => m.ar),
|
|
2874
|
+
arDZ: () => import("date-fns/locale/ar-DZ").then((m) => m.arDZ),
|
|
2875
|
+
arMA: () => import("date-fns/locale/ar-MA").then((m) => m.arMA),
|
|
2876
|
+
arSA: () => import("date-fns/locale/ar-SA").then((m) => m.arSA),
|
|
2877
|
+
az: () => import("date-fns/locale/az").then((m) => m.az),
|
|
2878
|
+
be: () => import("date-fns/locale/be").then((m) => m.be),
|
|
2879
|
+
bg: () => import("date-fns/locale/bg").then((m) => m.bg),
|
|
2880
|
+
bn: () => import("date-fns/locale/bn").then((m) => m.bn),
|
|
2881
|
+
ca: () => import("date-fns/locale/ca").then((m) => m.ca),
|
|
2882
|
+
cs: () => import("date-fns/locale/cs").then((m) => m.cs),
|
|
2883
|
+
cy: () => import("date-fns/locale/cy").then((m) => m.cy),
|
|
2884
|
+
da: () => import("date-fns/locale/da").then((m) => m.da),
|
|
2885
|
+
de: () => import("date-fns/locale/de").then((m) => m.de),
|
|
2886
|
+
el: () => import("date-fns/locale/el").then((m) => m.el),
|
|
2887
|
+
enAU: () => import("date-fns/locale/en-AU").then((m) => m.enAU),
|
|
2888
|
+
enCA: () => import("date-fns/locale/en-CA").then((m) => m.enCA),
|
|
2889
|
+
enGB: () => import("date-fns/locale/en-GB").then((m) => m.enGB),
|
|
2890
|
+
enIN: () => import("date-fns/locale/en-IN").then((m) => m.enIN),
|
|
2891
|
+
enNZ: () => import("date-fns/locale/en-NZ").then((m) => m.enNZ),
|
|
2892
|
+
enUS: () => import("date-fns/locale/en-US").then((m) => m.enUS),
|
|
2893
|
+
eo: () => import("date-fns/locale/eo").then((m) => m.eo),
|
|
2894
|
+
es: () => import("date-fns/locale/es").then((m) => m.es),
|
|
2895
|
+
et: () => import("date-fns/locale/et").then((m) => m.et),
|
|
2896
|
+
eu: () => import("date-fns/locale/eu").then((m) => m.eu),
|
|
2897
|
+
faIR: () => import("date-fns/locale/fa-IR").then((m) => m.faIR),
|
|
2898
|
+
fi: () => import("date-fns/locale/fi").then((m) => m.fi),
|
|
2899
|
+
fr: () => import("date-fns/locale/fr").then((m) => m.fr),
|
|
2900
|
+
frCA: () => import("date-fns/locale/fr-CA").then((m) => m.frCA),
|
|
2901
|
+
frCH: () => import("date-fns/locale/fr-CH").then((m) => m.frCH),
|
|
2902
|
+
gd: () => import("date-fns/locale/gd").then((m) => m.gd),
|
|
2903
|
+
gl: () => import("date-fns/locale/gl").then((m) => m.gl),
|
|
2904
|
+
gu: () => import("date-fns/locale/gu").then((m) => m.gu),
|
|
2905
|
+
he: () => import("date-fns/locale/he").then((m) => m.he),
|
|
2906
|
+
hi: () => import("date-fns/locale/hi").then((m) => m.hi),
|
|
2907
|
+
hr: () => import("date-fns/locale/hr").then((m) => m.hr),
|
|
2908
|
+
hu: () => import("date-fns/locale/hu").then((m) => m.hu),
|
|
2909
|
+
hy: () => import("date-fns/locale/hy").then((m) => m.hy),
|
|
2910
|
+
id: () => import("date-fns/locale/id").then((m) => m.id),
|
|
2911
|
+
is: () => import("date-fns/locale/is").then((m) => m.is),
|
|
2912
|
+
it: () => import("date-fns/locale/it").then((m) => m.it),
|
|
2913
|
+
ja: () => import("date-fns/locale/ja").then((m) => m.ja),
|
|
2914
|
+
ka: () => import("date-fns/locale/ka").then((m) => m.ka),
|
|
2915
|
+
kk: () => import("date-fns/locale/kk").then((m) => m.kk),
|
|
2916
|
+
kn: () => import("date-fns/locale/kn").then((m) => m.kn),
|
|
2917
|
+
ko: () => import("date-fns/locale/ko").then((m) => m.ko),
|
|
2918
|
+
lb: () => import("date-fns/locale/lb").then((m) => m.lb),
|
|
2919
|
+
lt: () => import("date-fns/locale/lt").then((m) => m.lt),
|
|
2920
|
+
lv: () => import("date-fns/locale/lv").then((m) => m.lv),
|
|
2921
|
+
mk: () => import("date-fns/locale/mk").then((m) => m.mk),
|
|
2922
|
+
ms: () => import("date-fns/locale/ms").then((m) => m.ms),
|
|
2923
|
+
mt: () => import("date-fns/locale/mt").then((m) => m.mt),
|
|
2924
|
+
nb: () => import("date-fns/locale/nb").then((m) => m.nb),
|
|
2925
|
+
nl: () => import("date-fns/locale/nl").then((m) => m.nl),
|
|
2926
|
+
nlBE: () => import("date-fns/locale/nl-BE").then((m) => m.nlBE),
|
|
2927
|
+
nn: () => import("date-fns/locale/nn").then((m) => m.nn),
|
|
2928
|
+
pl: () => import("date-fns/locale/pl").then((m) => m.pl),
|
|
2929
|
+
pt: () => import("date-fns/locale/pt").then((m) => m.pt),
|
|
2930
|
+
ptBR: () => import("date-fns/locale/pt-BR").then((m) => m.ptBR),
|
|
2931
|
+
ro: () => import("date-fns/locale/ro").then((m) => m.ro),
|
|
2932
|
+
ru: () => import("date-fns/locale/ru").then((m) => m.ru),
|
|
2933
|
+
sk: () => import("date-fns/locale/sk").then((m) => m.sk),
|
|
2934
|
+
sl: () => import("date-fns/locale/sl").then((m) => m.sl),
|
|
2935
|
+
sr: () => import("date-fns/locale/sr").then((m) => m.sr),
|
|
2936
|
+
srLatn: () => import("date-fns/locale/sr-Latn").then((m) => m.srLatn),
|
|
2937
|
+
sv: () => import("date-fns/locale/sv").then((m) => m.sv),
|
|
2938
|
+
ta: () => import("date-fns/locale/ta").then((m) => m.ta),
|
|
2939
|
+
te: () => import("date-fns/locale/te").then((m) => m.te),
|
|
2940
|
+
th: () => import("date-fns/locale/th").then((m) => m.th),
|
|
2941
|
+
tr: () => import("date-fns/locale/tr").then((m) => m.tr),
|
|
2942
|
+
ug: () => import("date-fns/locale/ug").then((m) => m.ug),
|
|
2943
|
+
uk: () => import("date-fns/locale/uk").then((m) => m.uk),
|
|
2944
|
+
uz: () => import("date-fns/locale/uz").then((m) => m.uz),
|
|
2945
|
+
vi: () => import("date-fns/locale/vi").then((m) => m.vi),
|
|
2946
|
+
zhCN: () => import("date-fns/locale/zh-CN").then((m) => m.zhCN),
|
|
2947
|
+
zhTW: () => import("date-fns/locale/zh-TW").then((m) => m.zhTW)
|
|
2948
|
+
};
|
|
2949
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2950
|
+
/** The locale if it has already been fetched, otherwise `undefined`. Never fetches. */
|
|
2951
|
+
function getLoadedDateFnsLocale(locale) {
|
|
2952
|
+
return locale ? cache.get(locale) : void 0;
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Fetch a date-fns locale. Resolves to `undefined` for a locale date-fns does
|
|
2956
|
+
* not ship, which is what indexing the namespace used to yield.
|
|
2957
|
+
*/
|
|
2958
|
+
async function loadDateFnsLocale(locale) {
|
|
2959
|
+
if (!locale) return void 0;
|
|
2960
|
+
const cached = cache.get(locale);
|
|
2961
|
+
if (cached) return cached;
|
|
2962
|
+
const loader = dateFnsLocaleLoaders[locale];
|
|
2963
|
+
if (!loader) return void 0;
|
|
2964
|
+
const loaded = await loader();
|
|
2965
|
+
cache.set(locale, loaded);
|
|
2966
|
+
return loaded;
|
|
2967
|
+
}
|
|
2968
|
+
//#endregion
|
|
2834
2969
|
//#region src/preview/components/DatePreview.tsx
|
|
2835
2970
|
/**
|
|
2836
2971
|
* @group Preview components
|
|
2837
2972
|
*/
|
|
2838
2973
|
function DatePreview({ date, mode = "date_time", timezone }) {
|
|
2839
2974
|
const customizationController = useCustomizationController();
|
|
2840
|
-
const
|
|
2975
|
+
const configuredLocale = customizationController?.locale;
|
|
2976
|
+
const [dateUtilsLocale, setDateUtilsLocale] = React.useState(() => getLoadedDateFnsLocale(configuredLocale));
|
|
2977
|
+
React.useEffect(() => {
|
|
2978
|
+
let cancelled = false;
|
|
2979
|
+
loadDateFnsLocale(configuredLocale).then((loaded) => {
|
|
2980
|
+
if (!cancelled) setDateUtilsLocale(loaded);
|
|
2981
|
+
}).catch((error) => console.error(`[rebase] could not load the "${configuredLocale}" date locale`, error));
|
|
2982
|
+
return () => {
|
|
2983
|
+
cancelled = true;
|
|
2984
|
+
};
|
|
2985
|
+
}, [configuredLocale]);
|
|
2841
2986
|
if (!date || !(date instanceof Date) || isNaN(date.getTime())) return /* @__PURE__ */ jsx(Fragment, {});
|
|
2842
2987
|
if (timezone) {
|
|
2843
2988
|
const options = {
|
|
@@ -3125,42 +3270,44 @@ var PropertyPreview = React.memo(function PropertyPreview(props) {
|
|
|
3125
3270
|
});
|
|
3126
3271
|
else content = buildWrongValueType(propertyKey, property.type, value);
|
|
3127
3272
|
else content = /* @__PURE__ */ jsx(EmptyValue, {});
|
|
3128
|
-
else if (property.type === "relation")
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
className: "w-full",
|
|
3147
|
-
children:
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3273
|
+
else if (property.type === "relation") {
|
|
3274
|
+
const relationTargetPath = getRelationTargetPath(property);
|
|
3275
|
+
if (!value) content = /* @__PURE__ */ jsx(EmptyValue, {});
|
|
3276
|
+
else if (Array.isArray(value)) {
|
|
3277
|
+
const relations = value.map((item) => normalizeToEntityRelation(item, "relation", relationTargetPath)).filter(Boolean);
|
|
3278
|
+
const renderRelation = (entityRelation, index) => /* @__PURE__ */ jsx(RelationPreview, {
|
|
3279
|
+
disabled: !property.relation,
|
|
3280
|
+
previewProperties: property.admin?.previewProperties,
|
|
3281
|
+
includeId: property.admin?.includeId,
|
|
3282
|
+
includeEntityLink: property.admin?.includeEntityLink,
|
|
3283
|
+
size: "small",
|
|
3284
|
+
relation: entityRelation,
|
|
3285
|
+
textOnly: props.textOnly
|
|
3286
|
+
}, `preview_rel_${propertyKey}_${index}`);
|
|
3287
|
+
content = nested || props.textOnly ? /* @__PURE__ */ jsx(InlineEntityListPreview, {
|
|
3288
|
+
items: relations,
|
|
3289
|
+
renderItem: renderRelation
|
|
3290
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
3291
|
+
className: "flex flex-col w-full gap-0.5",
|
|
3292
|
+
children: relations.map((entityRelation, index) => /* @__PURE__ */ jsx("div", {
|
|
3293
|
+
className: "w-full",
|
|
3294
|
+
children: renderRelation(entityRelation, index)
|
|
3295
|
+
}, `preview_rel_wrapper_${propertyKey}_${index}`))
|
|
3296
|
+
});
|
|
3297
|
+
} else {
|
|
3298
|
+
const relationValue = normalizeToEntityRelation(value, "relation", relationTargetPath);
|
|
3299
|
+
if (relationValue) content = /* @__PURE__ */ jsx(RelationPreview, {
|
|
3300
|
+
disabled: !property.relation,
|
|
3301
|
+
previewProperties: property.admin?.previewProperties,
|
|
3302
|
+
includeId: property.admin?.includeId,
|
|
3303
|
+
includeEntityLink: property.admin?.includeEntityLink,
|
|
3304
|
+
size: props.size,
|
|
3305
|
+
relation: relationValue,
|
|
3306
|
+
textOnly: props.textOnly
|
|
3307
|
+
});
|
|
3308
|
+
else content = buildWrongValueType(propertyKey, property.type, value);
|
|
3309
|
+
}
|
|
3310
|
+
} else if (property.type === "boolean") if (typeof value === "boolean") content = /* @__PURE__ */ jsx(BooleanPreview, {
|
|
3164
3311
|
value,
|
|
3165
3312
|
size,
|
|
3166
3313
|
property,
|
|
@@ -3329,14 +3476,15 @@ function ReadOnlyFieldBinding({ propertyKey, value, error, showError, minimalist
|
|
|
3329
3476
|
icon: getIconForProperty(property, "small"),
|
|
3330
3477
|
required: property.validation?.required,
|
|
3331
3478
|
title: property.name ?? propertyKey,
|
|
3332
|
-
className: "h-8 text-text-secondary dark:text-text-secondary-dark ml-3
|
|
3479
|
+
className: "h-8 text-text-secondary dark:text-text-secondary-dark ml-3"
|
|
3333
3480
|
}),
|
|
3334
3481
|
/* @__PURE__ */ jsx("div", {
|
|
3335
|
-
className: cls("w-full overflow-x-scroll no-scrollbar flex items-center", skipCardWrapper ? "" : "rounded-lg border border-surface-200 dark:border-surface-700 px-
|
|
3482
|
+
className: cls("w-full overflow-x-scroll no-scrollbar flex items-center", skipCardWrapper ? "" : "rounded-lg border border-surface-200 dark:border-surface-700 px-3 min-h-12 opacity-80"),
|
|
3336
3483
|
children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyPreview, {
|
|
3337
3484
|
propertyKey,
|
|
3338
3485
|
value,
|
|
3339
3486
|
property,
|
|
3487
|
+
hideLabel: hideLabel || !minimalistView,
|
|
3340
3488
|
size: "medium"
|
|
3341
3489
|
}) })
|
|
3342
3490
|
}),
|
|
@@ -3412,7 +3560,7 @@ function ArrayCustomShapedFieldBinding({ propertyKey, value, error, showError, i
|
|
|
3412
3560
|
]
|
|
3413
3561
|
})] });
|
|
3414
3562
|
const body = (resolvedProperties ?? []).map((childProperty, index) => {
|
|
3415
|
-
const thisDisabled = isReadOnly(childProperty) ||
|
|
3563
|
+
const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
|
|
3416
3564
|
return /* @__PURE__ */ jsx("div", {
|
|
3417
3565
|
className: "pb-4",
|
|
3418
3566
|
children: /* @__PURE__ */ jsx(PropertyFieldBinding, {
|
|
@@ -3562,9 +3710,13 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
|
|
|
3562
3710
|
const [currentSearch, setCurrentSearch] = useState("");
|
|
3563
3711
|
const offsetRef = useRef(0);
|
|
3564
3712
|
const userCacheRef = useRef(/* @__PURE__ */ new Map());
|
|
3713
|
+
const requestIdRef = useRef(0);
|
|
3714
|
+
const searchTimeoutRef = useRef(null);
|
|
3565
3715
|
const fetchUsers = useCallback(async (searchStr, offset, append) => {
|
|
3566
3716
|
const apiBase = apiBaseOf(client);
|
|
3567
3717
|
if (!apiBase) return;
|
|
3718
|
+
const requestId = ++requestIdRef.current;
|
|
3719
|
+
const superseded = () => requestId !== requestIdRef.current;
|
|
3568
3720
|
setIsLoading(true);
|
|
3569
3721
|
try {
|
|
3570
3722
|
const token = await getAuthToken?.();
|
|
@@ -3576,6 +3728,7 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
|
|
|
3576
3728
|
const response = await fetch(`${apiBase}/admin/users?${params}`, { headers: token ? { Authorization: `Bearer ${token}` } : {} });
|
|
3577
3729
|
if (!response.ok) throw new Error("Failed to fetch users");
|
|
3578
3730
|
const data = await response.json();
|
|
3731
|
+
if (superseded()) return;
|
|
3579
3732
|
const newItems = (data.data ?? data.users ?? []).map((r) => {
|
|
3580
3733
|
const user = {
|
|
3581
3734
|
uid: r.uid ?? r.id ?? "",
|
|
@@ -3597,9 +3750,10 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
|
|
|
3597
3750
|
setHasMore(newItems.length >= pageSize);
|
|
3598
3751
|
offsetRef.current = offset + newItems.length;
|
|
3599
3752
|
} catch {
|
|
3753
|
+
if (superseded()) return;
|
|
3600
3754
|
setHasMore(false);
|
|
3601
3755
|
} finally {
|
|
3602
|
-
setIsLoading(false);
|
|
3756
|
+
if (!superseded()) setIsLoading(false);
|
|
3603
3757
|
}
|
|
3604
3758
|
}, [
|
|
3605
3759
|
client?.baseUrl,
|
|
@@ -3611,15 +3765,24 @@ function useUserSelector({ pageSize = DEFAULT_PAGE_SIZE$2 } = {}) {
|
|
|
3611
3765
|
offsetRef.current = 0;
|
|
3612
3766
|
fetchUsers("", 0, false);
|
|
3613
3767
|
}, [fetchUsers]);
|
|
3768
|
+
const search = useCallback((searchStr) => {
|
|
3769
|
+
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
|
3770
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
3771
|
+
setCurrentSearch(searchStr);
|
|
3772
|
+
offsetRef.current = 0;
|
|
3773
|
+
fetchUsers(searchStr, 0, false);
|
|
3774
|
+
}, searchStr.trim() ? 300 : 0);
|
|
3775
|
+
}, [fetchUsers]);
|
|
3776
|
+
useEffect(() => {
|
|
3777
|
+
return () => {
|
|
3778
|
+
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current);
|
|
3779
|
+
};
|
|
3780
|
+
}, []);
|
|
3614
3781
|
return {
|
|
3615
3782
|
items,
|
|
3616
3783
|
isLoading,
|
|
3617
3784
|
hasMore,
|
|
3618
|
-
search
|
|
3619
|
-
setCurrentSearch(searchStr);
|
|
3620
|
-
offsetRef.current = 0;
|
|
3621
|
-
fetchUsers(searchStr, 0, false);
|
|
3622
|
-
}, [fetchUsers]),
|
|
3785
|
+
search,
|
|
3623
3786
|
loadMore: useCallback(() => {
|
|
3624
3787
|
fetchUsers(currentSearch, offsetRef.current, true);
|
|
3625
3788
|
}, [fetchUsers, currentSearch]),
|
|
@@ -3632,6 +3795,7 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
|
|
|
3632
3795
|
const { items: availableItems, isLoading, hasMore, search, loadMore, getUser } = useUserSelector({ pageSize });
|
|
3633
3796
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
3634
3797
|
const [searchString, setSearchString] = useState("");
|
|
3798
|
+
const contextPortalContainer = usePortalContainer();
|
|
3635
3799
|
const scrollContainerRef = useRef(null);
|
|
3636
3800
|
const sentinelRef = useRef(null);
|
|
3637
3801
|
const observerRef = useRef(null);
|
|
@@ -3643,7 +3807,9 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
|
|
|
3643
3807
|
}, [ref]);
|
|
3644
3808
|
const contentRef = useRef(null);
|
|
3645
3809
|
const searchInputRef = useRef(null);
|
|
3646
|
-
const
|
|
3810
|
+
const listedUser = (value ? getUser(value) ?? null : null) ?? (value ? availableItems.find((i) => i.uid === value)?.user ?? null : null);
|
|
3811
|
+
const fetchedUser = useResolvedUser(value && !listedUser ? value : void 0);
|
|
3812
|
+
const resolvedUser = listedUser ?? fetchedUser;
|
|
3647
3813
|
const sentinelCallbackRef = useCallback((node) => {
|
|
3648
3814
|
if (observerRef.current) {
|
|
3649
3815
|
observerRef.current.disconnect();
|
|
@@ -3723,9 +3889,8 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
|
|
|
3723
3889
|
document.removeEventListener("keydown", handleKey, true);
|
|
3724
3890
|
};
|
|
3725
3891
|
}, [isPopoverOpen]);
|
|
3726
|
-
useInjectStyles("UserSelector", " [cmdk-group] { max-height: 40vh; overflow-y: auto; } ");
|
|
3727
3892
|
const resolvedPlaceholder = placeholder || /* @__PURE__ */ jsx(EmptyValue, { className: "ml-2" });
|
|
3728
|
-
const portalContainer = typeof document !== "undefined" ? document.body : void 0;
|
|
3893
|
+
const portalContainer = contextPortalContainer ?? (typeof document !== "undefined" ? document.body : void 0) ?? void 0;
|
|
3729
3894
|
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(PopoverPrimitive.Root, {
|
|
3730
3895
|
open: isPopoverOpen,
|
|
3731
3896
|
onOpenChange: handleRootOpenChange,
|
|
@@ -3753,6 +3918,9 @@ var UserSelector = React$1.forwardRef(({ value, size = "medium", onValueChange,
|
|
|
3753
3918
|
children: [resolvedUser ? /* @__PURE__ */ jsx("div", {
|
|
3754
3919
|
className: "flex flex-row items-center gap-1 truncate flex-1 min-w-0 mr-2",
|
|
3755
3920
|
children: /* @__PURE__ */ jsx(UserDisplay, { user: resolvedUser })
|
|
3921
|
+
}) : value ? /* @__PURE__ */ jsx("span", {
|
|
3922
|
+
className: "text-sm truncate text-text-primary dark:text-text-primary-dark",
|
|
3923
|
+
children: value
|
|
3756
3924
|
}) : /* @__PURE__ */ jsx("span", {
|
|
3757
3925
|
className: "text-sm text-text-secondary dark:text-text-secondary-dark",
|
|
3758
3926
|
children: resolvedPlaceholder
|
|
@@ -4430,13 +4598,15 @@ function relationCardinality$2(relation) {
|
|
|
4430
4598
|
if (relation.kind === "belongsTo" || relation.kind === "hasOne") return "one";
|
|
4431
4599
|
return relation.cardinality;
|
|
4432
4600
|
}
|
|
4433
|
-
var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChange, invisible, disabled, placeholder, useChips = true, className, relation, multiple: multipleOverride, fixedFilter, pageSize, emptyPlaceholder, searchPlaceholder = "Search...", noResultsText = "No
|
|
4601
|
+
var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChange, invisible, disabled, placeholder, useChips = true, className, relation, multiple: multipleOverride, fixedFilter, pageSize, emptyPlaceholder, searchPlaceholder = "Search...", noResultsText = "No matches.", emptyText = "Select…", emptyCollectionText, loadingText = "Loading..." }, ref) => {
|
|
4434
4602
|
const collection = relation.target();
|
|
4435
4603
|
const dataClient = useData();
|
|
4436
4604
|
const sidePanelController = useSidePanel();
|
|
4605
|
+
const contextPortalContainer = usePortalContainer();
|
|
4437
4606
|
const multiple = multipleOverride ?? relationCardinality$2(relation) === "many";
|
|
4438
4607
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
|
4439
4608
|
const isPopoverOpenRef = useRef(false);
|
|
4609
|
+
const [listRequested, setListRequested] = useState(false);
|
|
4440
4610
|
const [selectedItems, setSelectedItems] = useState([]);
|
|
4441
4611
|
const [isLoadingSelectedItems, setIsLoadingSelectedItems] = useState(false);
|
|
4442
4612
|
const [searchString, setSearchString] = useState("");
|
|
@@ -4446,7 +4616,8 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
|
|
|
4446
4616
|
path: getCollectionDataPath(collection),
|
|
4447
4617
|
collection,
|
|
4448
4618
|
fixedFilter,
|
|
4449
|
-
pageSize
|
|
4619
|
+
pageSize,
|
|
4620
|
+
enabled: listRequested
|
|
4450
4621
|
});
|
|
4451
4622
|
const scrollContainerRef = useRef(null);
|
|
4452
4623
|
const sentinelRef = useRef(null);
|
|
@@ -4639,6 +4810,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
|
|
|
4639
4810
|
if (disabled) return;
|
|
4640
4811
|
if (next) {
|
|
4641
4812
|
pinnedIdsRef.current = new Set(selectedItems.map((i) => String(i.id)));
|
|
4813
|
+
setListRequested(true);
|
|
4642
4814
|
setIsPopoverOpen(true);
|
|
4643
4815
|
isPopoverOpenRef.current = true;
|
|
4644
4816
|
}
|
|
@@ -4669,14 +4841,13 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
|
|
|
4669
4841
|
document.removeEventListener("keydown", handleKey, true);
|
|
4670
4842
|
};
|
|
4671
4843
|
}, [isPopoverOpen]);
|
|
4672
|
-
useInjectStyles("RelationSelector", " [cmdk-group] { max-height: 40vh; overflow-y: auto; } ");
|
|
4673
4844
|
const closePopover = useCallback(() => {
|
|
4674
4845
|
setIsPopoverOpen(false);
|
|
4675
4846
|
isPopoverOpenRef.current = false;
|
|
4676
4847
|
pinnedIdsRef.current = null;
|
|
4677
4848
|
}, []);
|
|
4678
|
-
const resolvedPlaceholder = placeholder || emptyPlaceholder ||
|
|
4679
|
-
const portalContainer = typeof document !== "undefined" ? document.body : void 0;
|
|
4849
|
+
const resolvedPlaceholder = placeholder || emptyPlaceholder || emptyText;
|
|
4850
|
+
const portalContainer = contextPortalContainer ?? (typeof document !== "undefined" ? document.body : void 0) ?? void 0;
|
|
4680
4851
|
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs(PopoverPrimitive.Root, {
|
|
4681
4852
|
open: isPopoverOpen,
|
|
4682
4853
|
onOpenChange: handleRootOpenChange,
|
|
@@ -4692,6 +4863,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
|
|
|
4692
4863
|
disabled,
|
|
4693
4864
|
onClick: () => {
|
|
4694
4865
|
if (disabled) return;
|
|
4866
|
+
setListRequested(true);
|
|
4695
4867
|
setIsPopoverOpen((o) => {
|
|
4696
4868
|
const next = !o;
|
|
4697
4869
|
isPopoverOpenRef.current = next;
|
|
@@ -4848,7 +5020,7 @@ var RelationSelector = React$1.forwardRef(({ value, size = "medium", onValueChan
|
|
|
4848
5020
|
}),
|
|
4849
5021
|
!isLoading && availableItems.length === 0 && /* @__PURE__ */ jsx(Command.Empty, {
|
|
4850
5022
|
className: "px-4 py-6 text-center text-text-secondary dark:text-text-secondary-dark",
|
|
4851
|
-
children: noResultsText
|
|
5023
|
+
children: searchString ? noResultsText : emptyCollectionText ?? `No ${collection.name ?? "entries"} yet.`
|
|
4852
5024
|
}),
|
|
4853
5025
|
/* @__PURE__ */ jsxs(Command.Group, { children: [
|
|
4854
5026
|
(() => {
|
|
@@ -5079,14 +5251,16 @@ function getTableBindingForProperty(property, selected) {
|
|
|
5079
5251
|
allowScroll: false
|
|
5080
5252
|
};
|
|
5081
5253
|
} else if (property.type === "relation") {
|
|
5082
|
-
if (property.relation)
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5254
|
+
if (property.relation) {
|
|
5255
|
+
if (property.admin?.widget === "dialog") return {
|
|
5256
|
+
Component: RelationDialogBindingComponent,
|
|
5257
|
+
allowScroll: false
|
|
5258
|
+
};
|
|
5259
|
+
else if (selected) return {
|
|
5260
|
+
Component: RelationSelectorBindingComponent,
|
|
5261
|
+
allowScroll: false
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5090
5264
|
} else if (property.type === "array") {
|
|
5091
5265
|
const arrayProperty = property;
|
|
5092
5266
|
if (!arrayProperty.of && !arrayProperty.oneOf) throw Error("You need to specify an 'of' or 'oneOf' prop (or specify a custom field) in your array property");
|
|
@@ -5565,7 +5739,7 @@ var PropertyTableCell = React.memo(function PropertyTableCell({ propertyKey, cus
|
|
|
5565
5739
|
const customPreview = Boolean(property.admin?.Preview);
|
|
5566
5740
|
const readOnlyProperty = isReadOnly(property);
|
|
5567
5741
|
const disabledTooltip = typeof property.admin?.disabled === "object" ? property.admin?.disabled.disabledMessage : void 0;
|
|
5568
|
-
const disabled = readonly || disabledProp ||
|
|
5742
|
+
const disabled = readonly || disabledProp || isDisabled(property);
|
|
5569
5743
|
const validation = useMemo(() => mapPropertyToZod({
|
|
5570
5744
|
property,
|
|
5571
5745
|
entityId: entity.id,
|
|
@@ -6049,11 +6223,14 @@ function getSubcollectionColumnId(collection) {
|
|
|
6049
6223
|
//#region src/components/CollectionTableBinding/column_utils.tsx
|
|
6050
6224
|
function propertiesToColumns({ properties, sortable, fixedFilter, engine, AdditionalHeaderWidget }) {
|
|
6051
6225
|
const disabledFilter = Boolean(fixedFilter);
|
|
6052
|
-
return Object.entries(properties).flatMap(([key, property]) => getColumnKeysForProperty(property, key)).
|
|
6226
|
+
return Object.entries(properties).flatMap(([key, property]) => getColumnKeysForProperty(property, key)).flatMap(({ key, disabled }) => {
|
|
6053
6227
|
const property = getResolvedPropertyInPath(properties, key);
|
|
6054
|
-
if (!property)
|
|
6228
|
+
if (!property) {
|
|
6229
|
+
console.warn(`No property found in path "${key}" — skipping that column.`);
|
|
6230
|
+
return [];
|
|
6231
|
+
}
|
|
6055
6232
|
const filterable = filterableProperty(property, false, engine);
|
|
6056
|
-
return {
|
|
6233
|
+
return [{
|
|
6057
6234
|
key,
|
|
6058
6235
|
align: getTableCellAlignment(property),
|
|
6059
6236
|
icon: getIconForProperty(property, "small"),
|
|
@@ -6071,7 +6248,7 @@ function propertiesToColumns({ properties, sortable, fixedFilter, engine, Additi
|
|
|
6071
6248
|
propertyKey: key,
|
|
6072
6249
|
onHover
|
|
6073
6250
|
}) : void 0
|
|
6074
|
-
};
|
|
6251
|
+
}];
|
|
6075
6252
|
});
|
|
6076
6253
|
}
|
|
6077
6254
|
/**
|
|
@@ -6242,6 +6419,7 @@ function StringNumberFilterField({ name, value, setValue, type, isArray, enumVal
|
|
|
6242
6419
|
children: [
|
|
6243
6420
|
!enumValues && /* @__PURE__ */ jsx(TextField, {
|
|
6244
6421
|
size: "medium",
|
|
6422
|
+
"aria-label": `${title ?? name} filter value`,
|
|
6245
6423
|
type: type === "number" ? "number" : void 0,
|
|
6246
6424
|
value: internalValue !== void 0 && internalValue != null ? String(internalValue) : "",
|
|
6247
6425
|
disabled: isNullOperation,
|
|
@@ -7076,7 +7254,7 @@ var CollectionTableBinding = function CollectionTableBinding({ className, style,
|
|
|
7076
7254
|
propertyKey,
|
|
7077
7255
|
entity
|
|
7078
7256
|
}) ?? columnCustom?.resolvedProperty;
|
|
7079
|
-
if (!property
|
|
7257
|
+
if (!property || !isDisabled(property)) disabled = false;
|
|
7080
7258
|
if (!property) return null;
|
|
7081
7259
|
return /* @__PURE__ */ jsx(ErrorBoundary, { children: entity ? /* @__PURE__ */ jsx(PropertyTableCell, {
|
|
7082
7260
|
readonly: !inlineEditing,
|
|
@@ -7398,6 +7576,35 @@ function withViewMode(url, search) {
|
|
|
7398
7576
|
return `${base}${base.includes("?") ? "&" : "?"}${VIEW_MODE_PARAM}=${viewMode}${hash}`;
|
|
7399
7577
|
}
|
|
7400
7578
|
/**
|
|
7579
|
+
* Carry the whole of the collection list's URL state onto a navigation target.
|
|
7580
|
+
*
|
|
7581
|
+
* `withViewMode` exists because losing the view mode mid-session is jarring.
|
|
7582
|
+
* Everything else the list writes to the URL — the search string, the filters,
|
|
7583
|
+
* the sort — is state the user built up deliberately, and losing *that* is
|
|
7584
|
+
* worse: opening a record from a search and pressing back returned an
|
|
7585
|
+
* unfiltered list with an empty search box, so the search had to be retyped
|
|
7586
|
+
* for every record they wanted to look at.
|
|
7587
|
+
*
|
|
7588
|
+
* Copies every param the list currently has rather than an allowlist, because
|
|
7589
|
+
* filters are encoded under the field's own name — there is no fixed set to
|
|
7590
|
+
* list. Params already on the target win: the caller is more specific.
|
|
7591
|
+
*
|
|
7592
|
+
* @param url the URL to decorate. May carry a query string and/or a hash.
|
|
7593
|
+
* @param search query string to read from. Defaults to the browser location.
|
|
7594
|
+
*/
|
|
7595
|
+
function withListState(url, search) {
|
|
7596
|
+
const source = search ?? (typeof window !== "undefined" ? window.location.search : "");
|
|
7597
|
+
const carried = new URLSearchParams(source);
|
|
7598
|
+
if ([...carried].length === 0) return url;
|
|
7599
|
+
const hashIndex = url.indexOf("#");
|
|
7600
|
+
const hash = hashIndex >= 0 ? url.substring(hashIndex) : "";
|
|
7601
|
+
const [pathname, existing] = (hashIndex >= 0 ? url.substring(0, hashIndex) : url).split("?");
|
|
7602
|
+
const target = new URLSearchParams(existing ?? "");
|
|
7603
|
+
for (const [key, value] of carried) if (!target.has(key)) target.set(key, value);
|
|
7604
|
+
const query = target.toString();
|
|
7605
|
+
return query ? `${pathname}?${query}${hash}` : `${pathname}${hash}`;
|
|
7606
|
+
}
|
|
7607
|
+
/**
|
|
7401
7608
|
* Resolve the view mode for a collection, honouring the documented priority:
|
|
7402
7609
|
* URL param > saved user config > collection default.
|
|
7403
7610
|
*/
|
|
@@ -7445,7 +7652,7 @@ function navigateToEntity({ openEntityMode, collection, entityId, copy, path, se
|
|
|
7445
7652
|
else {
|
|
7446
7653
|
let to = navigation.buildUrlCollectionPath(entityId ? `${path ?? path}/${entityId}` : path ?? path);
|
|
7447
7654
|
if (entityId && selectedTab) to += `/${selectedTab}`;
|
|
7448
|
-
to =
|
|
7655
|
+
to = withListState(to);
|
|
7449
7656
|
if (!entityId) to += "#new";
|
|
7450
7657
|
if (copy) to += "#copy";
|
|
7451
7658
|
const navigateOptions = {};
|
|
@@ -7575,15 +7782,6 @@ function FieldCaption({ error, children }) {
|
|
|
7575
7782
|
//#endregion
|
|
7576
7783
|
//#region src/components/SearchIconsView.tsx
|
|
7577
7784
|
var UPDATE_SEARCH_INDEX_WAIT_MS = 220;
|
|
7578
|
-
function toPascalCase(str) {
|
|
7579
|
-
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
7580
|
-
}
|
|
7581
|
-
function resolveIcon(iconKey) {
|
|
7582
|
-
const allIcons = lucideIcons;
|
|
7583
|
-
let icon = allIcons[iconKey];
|
|
7584
|
-
if (!icon) icon = allIcons[toPascalCase(iconKey)];
|
|
7585
|
-
return icon ?? null;
|
|
7586
|
-
}
|
|
7587
7785
|
function SearchIconsView({ selectedIcon = "", onIconSelected }) {
|
|
7588
7786
|
const { t } = useTranslation();
|
|
7589
7787
|
const [keys, setKeys] = React.useState(null);
|
|
@@ -7609,21 +7807,20 @@ function SearchIconsView({ selectedIcon = "", onIconSelected }) {
|
|
|
7609
7807
|
placeholder: t("search_for_more_icons")
|
|
7610
7808
|
}), /* @__PURE__ */ jsx("div", {
|
|
7611
7809
|
className: "flex max-w-full flex-wrap mt-4",
|
|
7612
|
-
children: icons.map((icon) => {
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
children: /* @__PURE__ */ jsx(LucideIcon, { size: iconSize.medium })
|
|
7810
|
+
children: icons.map((icon) => /* @__PURE__ */ jsx(Tooltip, {
|
|
7811
|
+
title: icon,
|
|
7812
|
+
asChild: true,
|
|
7813
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
7814
|
+
shape: "square",
|
|
7815
|
+
toggled: selectedIcon === icon,
|
|
7816
|
+
onClick: onIconSelected ? () => onIconSelected(icon) : void 0,
|
|
7817
|
+
className: "box-content m-1",
|
|
7818
|
+
children: /* @__PURE__ */ jsx(LucideIconByName, {
|
|
7819
|
+
name: icon,
|
|
7820
|
+
size: iconSize.medium
|
|
7624
7821
|
})
|
|
7625
|
-
}
|
|
7626
|
-
})
|
|
7822
|
+
})
|
|
7823
|
+
}, icon))
|
|
7627
7824
|
})] });
|
|
7628
7825
|
}
|
|
7629
7826
|
//#endregion
|
|
@@ -7967,10 +8164,23 @@ function getWorksheetHeaders(worksheet) {
|
|
|
7967
8164
|
/**
|
|
7968
8165
|
* Take an object with keys of type `address.street`, `address.city` and
|
|
7969
8166
|
* convert it to an object with nested objects like `{ address: { street: ..., city: ... } }`
|
|
8167
|
+
*
|
|
8168
|
+
* Keys here are the **header row of an uploaded file**, so they are attacker
|
|
8169
|
+
* data. `res["__proto__"] = …` is the prototype setter rather than an own
|
|
8170
|
+
* property, so a column named `__proto__.polluted` walked out of the accumulator
|
|
8171
|
+
* and wrote onto `Object.prototype` for the life of the tab — and
|
|
8172
|
+
* `constructor.prototype.x` threw a `TypeError` that surfaced as an unreadable
|
|
8173
|
+
* file. Such a column is refused, not sanitised, the way `setIn` settled it
|
|
8174
|
+
* (`docs/bug-classes.md` class 22).
|
|
8175
|
+
*
|
|
7970
8176
|
* @param flatObj
|
|
7971
8177
|
*/
|
|
7972
8178
|
function unflattenObject(flatObj) {
|
|
7973
8179
|
return Object.keys(flatObj).reduce((nestedObj, key) => {
|
|
8180
|
+
if (pathTraversesPrototype(key)) {
|
|
8181
|
+
console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
|
|
8182
|
+
return nestedObj;
|
|
8183
|
+
}
|
|
7974
8184
|
let currentObj = nestedObj;
|
|
7975
8185
|
const keyParts = key.split(".");
|
|
7976
8186
|
keyParts.forEach((keyPart, i) => {
|
|
@@ -7992,6 +8202,10 @@ function unflattenObject(flatObj) {
|
|
|
7992
8202
|
}
|
|
7993
8203
|
function mapJsonParse(obj) {
|
|
7994
8204
|
return Object.keys(obj).reduce((acc, key) => {
|
|
8205
|
+
if (isPrototypePollutingKey(key)) {
|
|
8206
|
+
console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
|
|
8207
|
+
return acc;
|
|
8208
|
+
}
|
|
7995
8209
|
try {
|
|
7996
8210
|
acc[key] = JSON.parse(obj[key]);
|
|
7997
8211
|
} catch (e) {
|
|
@@ -8001,10 +8215,183 @@ function mapJsonParse(obj) {
|
|
|
8001
8215
|
}, {});
|
|
8002
8216
|
}
|
|
8003
8217
|
//#endregion
|
|
8218
|
+
//#region src/data_import/utils/csv.ts
|
|
8219
|
+
/**
|
|
8220
|
+
* Delimiters sniffed from the header row, most common first.
|
|
8221
|
+
*
|
|
8222
|
+
* A spreadsheet saved as "CSV" in a locale that uses the comma as a decimal
|
|
8223
|
+
* separator is semicolon-delimited, and a tab-separated file saved as `.csv`
|
|
8224
|
+
* happens often enough to be worth reading.
|
|
8225
|
+
*/
|
|
8226
|
+
var CANDIDATE_DELIMITERS = [
|
|
8227
|
+
",",
|
|
8228
|
+
";",
|
|
8229
|
+
" "
|
|
8230
|
+
];
|
|
8231
|
+
/**
|
|
8232
|
+
* Split CSV text into rows of raw cells, RFC 4180 style.
|
|
8233
|
+
*
|
|
8234
|
+
* Quoted fields may contain the delimiter, newlines and doubled quotes; this is
|
|
8235
|
+
* the exact inverse of what the export's `entryToCSVRow` writes, so a file this
|
|
8236
|
+
* admin produced reads back into the cells it was built from.
|
|
8237
|
+
*/
|
|
8238
|
+
function parseCsvRows(text, delimiter = ",") {
|
|
8239
|
+
const rows = [];
|
|
8240
|
+
let row = [];
|
|
8241
|
+
let field = "";
|
|
8242
|
+
let quoted = false;
|
|
8243
|
+
let inQuotes = false;
|
|
8244
|
+
let i = 0;
|
|
8245
|
+
const endField = () => {
|
|
8246
|
+
row.push(field);
|
|
8247
|
+
field = "";
|
|
8248
|
+
quoted = false;
|
|
8249
|
+
};
|
|
8250
|
+
while (i < text.length) {
|
|
8251
|
+
const char = text[i];
|
|
8252
|
+
if (inQuotes) {
|
|
8253
|
+
if (char === "\"") {
|
|
8254
|
+
if (text[i + 1] === "\"") {
|
|
8255
|
+
field += "\"";
|
|
8256
|
+
i += 2;
|
|
8257
|
+
continue;
|
|
8258
|
+
}
|
|
8259
|
+
inQuotes = false;
|
|
8260
|
+
i++;
|
|
8261
|
+
continue;
|
|
8262
|
+
}
|
|
8263
|
+
field += char;
|
|
8264
|
+
i++;
|
|
8265
|
+
continue;
|
|
8266
|
+
}
|
|
8267
|
+
if (char === "\"" && field === "") {
|
|
8268
|
+
inQuotes = true;
|
|
8269
|
+
quoted = true;
|
|
8270
|
+
i++;
|
|
8271
|
+
continue;
|
|
8272
|
+
}
|
|
8273
|
+
if (char === delimiter) {
|
|
8274
|
+
endField();
|
|
8275
|
+
i++;
|
|
8276
|
+
continue;
|
|
8277
|
+
}
|
|
8278
|
+
if (char === "\r") {
|
|
8279
|
+
i++;
|
|
8280
|
+
continue;
|
|
8281
|
+
}
|
|
8282
|
+
if (char === "\n") {
|
|
8283
|
+
endField();
|
|
8284
|
+
rows.push(row);
|
|
8285
|
+
row = [];
|
|
8286
|
+
i++;
|
|
8287
|
+
continue;
|
|
8288
|
+
}
|
|
8289
|
+
field += char;
|
|
8290
|
+
i++;
|
|
8291
|
+
}
|
|
8292
|
+
if (field !== "" || quoted || row.length > 0) {
|
|
8293
|
+
endField();
|
|
8294
|
+
rows.push(row);
|
|
8295
|
+
}
|
|
8296
|
+
return rows;
|
|
8297
|
+
}
|
|
8298
|
+
/**
|
|
8299
|
+
* Pick the delimiter that splits the header row into the most cells. A
|
|
8300
|
+
* single-column file matches nothing and stays on the comma.
|
|
8301
|
+
*/
|
|
8302
|
+
function detectCsvDelimiter(text) {
|
|
8303
|
+
const sample = text.slice(0, 64 * 1024);
|
|
8304
|
+
let best = CANDIDATE_DELIMITERS[0];
|
|
8305
|
+
let bestCount = 1;
|
|
8306
|
+
for (const candidate of CANDIDATE_DELIMITERS) {
|
|
8307
|
+
const count = parseCsvRows(sample, candidate)[0]?.length ?? 0;
|
|
8308
|
+
if (count > bestCount) {
|
|
8309
|
+
best = candidate;
|
|
8310
|
+
bestCount = count;
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
return best;
|
|
8314
|
+
}
|
|
8315
|
+
/**
|
|
8316
|
+
* Parse CSV text into one object per row, keyed by the header row.
|
|
8317
|
+
*
|
|
8318
|
+
* The picker accepts `.csv` and the dialog promises "Upload a CSV, Excel or JSON
|
|
8319
|
+
* file", but every non-JSON file used to be handed to ExcelJS'
|
|
8320
|
+
* `workbook.xlsx.load`, which is a zip reader — so every CSV failed with
|
|
8321
|
+
* "Can't find end of central directory : is this a zip file ?". The export
|
|
8322
|
+
* writes CSV by default, so the round trip could not close.
|
|
8323
|
+
*
|
|
8324
|
+
* A blank header keeps its column rather than collapsing it, so the cells to its
|
|
8325
|
+
* right stay under their own names.
|
|
8326
|
+
*/
|
|
8327
|
+
function parseCsvToObjects(text) {
|
|
8328
|
+
const cleaned = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
8329
|
+
const rows = parseCsvRows(cleaned, detectCsvDelimiter(cleaned)).filter((row) => row.some((cell) => cell.trim() !== ""));
|
|
8330
|
+
if (rows.length === 0) return {
|
|
8331
|
+
headers: [],
|
|
8332
|
+
data: []
|
|
8333
|
+
};
|
|
8334
|
+
const headers = rows[0].map((header, index) => header.trim() || `Column${index + 1}`);
|
|
8335
|
+
return {
|
|
8336
|
+
headers,
|
|
8337
|
+
data: rows.slice(1).map((cells) => {
|
|
8338
|
+
const obj = {};
|
|
8339
|
+
headers.forEach((header, index) => {
|
|
8340
|
+
if (isPrototypePollutingKey(header)) return;
|
|
8341
|
+
const cell = cells[index];
|
|
8342
|
+
if (cell === void 0) return;
|
|
8343
|
+
obj[header] = cell;
|
|
8344
|
+
});
|
|
8345
|
+
return obj;
|
|
8346
|
+
})
|
|
8347
|
+
};
|
|
8348
|
+
}
|
|
8349
|
+
//#endregion
|
|
8004
8350
|
//#region src/data_import/utils/file_to_json.ts
|
|
8351
|
+
var excelJsModule;
|
|
8352
|
+
function loadExcelJs() {
|
|
8353
|
+
excelJsModule ??= import("exceljs").then((mod) => mod.default ?? mod);
|
|
8354
|
+
return excelJsModule;
|
|
8355
|
+
}
|
|
8356
|
+
/**
|
|
8357
|
+
* Whether this file is delimited text rather than a workbook.
|
|
8358
|
+
*
|
|
8359
|
+
* Browsers report `text/csv`, `application/csv` or nothing at all for the same
|
|
8360
|
+
* `.csv`, so the extension is the reliable half of the test.
|
|
8361
|
+
*/
|
|
8362
|
+
function isCsvFile(file) {
|
|
8363
|
+
const name = (file.name ?? "").toLowerCase();
|
|
8364
|
+
if (name.endsWith(".csv") || name.endsWith(".tsv")) return true;
|
|
8365
|
+
return file.type === "text/csv" || file.type === "application/csv";
|
|
8366
|
+
}
|
|
8367
|
+
/** Turn the parsed cells of one sheet/file into the entity-shaped rows the import maps. */
|
|
8368
|
+
function toImportRows(rows) {
|
|
8369
|
+
return rows.map(mapJsonParse).map(unflattenObject);
|
|
8370
|
+
}
|
|
8005
8371
|
function convertFileToJson(file) {
|
|
8006
8372
|
return new Promise((resolve, reject) => {
|
|
8007
|
-
if (file
|
|
8373
|
+
if (isCsvFile(file)) {
|
|
8374
|
+
console.debug("Converting CSV file to JSON", file.name);
|
|
8375
|
+
const reader = new FileReader();
|
|
8376
|
+
reader.onload = function(e) {
|
|
8377
|
+
try {
|
|
8378
|
+
const { headers, data } = parseCsvToObjects(e.target?.result);
|
|
8379
|
+
if (headers.length === 0) {
|
|
8380
|
+
reject(/* @__PURE__ */ new Error("The CSV file is empty"));
|
|
8381
|
+
return;
|
|
8382
|
+
}
|
|
8383
|
+
resolve({
|
|
8384
|
+
data: toImportRows(data),
|
|
8385
|
+
propertiesOrder: headers
|
|
8386
|
+
});
|
|
8387
|
+
} catch (err) {
|
|
8388
|
+
console.error("Error parsing CSV file", err);
|
|
8389
|
+
reject(err);
|
|
8390
|
+
}
|
|
8391
|
+
};
|
|
8392
|
+
reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("Could not read the file"));
|
|
8393
|
+
reader.readAsText(file, "utf-8");
|
|
8394
|
+
} else if (file.type === "application/json") {
|
|
8008
8395
|
console.debug("Converting JSON file to JSON", file.name);
|
|
8009
8396
|
const reader = new FileReader();
|
|
8010
8397
|
reader.onload = function(e) {
|
|
@@ -8028,7 +8415,7 @@ function convertFileToJson(file) {
|
|
|
8028
8415
|
reader.onload = async function(e) {
|
|
8029
8416
|
try {
|
|
8030
8417
|
const buffer = e.target?.result;
|
|
8031
|
-
const workbook = new
|
|
8418
|
+
const workbook = new (await (loadExcelJs())).Workbook();
|
|
8032
8419
|
await workbook.xlsx.load(buffer);
|
|
8033
8420
|
const worksheet = workbook.worksheets[0];
|
|
8034
8421
|
if (!worksheet) {
|
|
@@ -8042,12 +8429,12 @@ function convertFileToJson(file) {
|
|
|
8042
8429
|
const obj = {};
|
|
8043
8430
|
row.eachCell({ includeEmpty: false }, (cell, colNumber) => {
|
|
8044
8431
|
const header = headers[colNumber - 1];
|
|
8045
|
-
if (header) obj[header] = cell.value;
|
|
8432
|
+
if (header && !isPrototypePollutingKey(header)) obj[header] = cell.value;
|
|
8046
8433
|
});
|
|
8047
8434
|
parsedData.push(obj);
|
|
8048
8435
|
});
|
|
8049
8436
|
resolve({
|
|
8050
|
-
data: parsedData
|
|
8437
|
+
data: toImportRows(parsedData),
|
|
8051
8438
|
propertiesOrder: headers
|
|
8052
8439
|
});
|
|
8053
8440
|
} catch (err) {
|
|
@@ -8096,44 +8483,153 @@ function ImportFileUpload({ onDataAdded }) {
|
|
|
8096
8483
|
});
|
|
8097
8484
|
}
|
|
8098
8485
|
//#endregion
|
|
8486
|
+
//#region src/data_import/utils/save_entities.ts
|
|
8487
|
+
/** Rows written per request, and per transaction where the data source has one. */
|
|
8488
|
+
var IMPORT_BATCH_SIZE = 25;
|
|
8489
|
+
/** The row a bulk write takes: the values, with the chosen id as a column. */
|
|
8490
|
+
function toBulkRow(entity) {
|
|
8491
|
+
const values = { ...entity.values ?? {} };
|
|
8492
|
+
if (entity.id !== void 0 && entity.id !== null && entity.id !== "") values.id = entity.id;
|
|
8493
|
+
return values;
|
|
8494
|
+
}
|
|
8495
|
+
function isBulkUnsupported(error) {
|
|
8496
|
+
if (error?.code === "BULK_UNSUPPORTED") return true;
|
|
8497
|
+
const message = error?.message ?? "";
|
|
8498
|
+
return /bulk writes are not supported|does not support bulk/i.test(message);
|
|
8499
|
+
}
|
|
8500
|
+
function isConflict(error) {
|
|
8501
|
+
if ((error?.status ?? error?.statusCode) === 409) return true;
|
|
8502
|
+
const code = error?.code;
|
|
8503
|
+
return code === "CONFLICT" || code === "23505";
|
|
8504
|
+
}
|
|
8505
|
+
/**
|
|
8506
|
+
* Write one batch, all-or-nothing wherever the data source can.
|
|
8507
|
+
*
|
|
8508
|
+
* `POST /<collection>/bulk` validates every row before the transaction opens,
|
|
8509
|
+
* names a failure by row index, and takes the `upsert` flag the preview screen
|
|
8510
|
+
* promises — none of which one `create` per row inside a `Promise.all` could
|
|
8511
|
+
* offer, since `Promise.all` does not cancel its siblings and every request was
|
|
8512
|
+
* its own transaction.
|
|
8513
|
+
*/
|
|
8514
|
+
async function writeBatch(accessor, batch, bulkUnsupported, written) {
|
|
8515
|
+
if (accessor.createMany && !bulkUnsupported.current) try {
|
|
8516
|
+
await accessor.createMany(batch.map(toBulkRow), { upsert: true });
|
|
8517
|
+
written.count = batch.length;
|
|
8518
|
+
return;
|
|
8519
|
+
} catch (e) {
|
|
8520
|
+
if (!isBulkUnsupported(e)) throw e;
|
|
8521
|
+
bulkUnsupported.current = true;
|
|
8522
|
+
}
|
|
8523
|
+
for (const entity of batch) {
|
|
8524
|
+
try {
|
|
8525
|
+
await accessor.create(entity.values ?? {}, entity.id);
|
|
8526
|
+
} catch (e) {
|
|
8527
|
+
if (entity.id === void 0 || !isConflict(e)) throw e;
|
|
8528
|
+
await accessor.update(entity.id, entity.values ?? {});
|
|
8529
|
+
}
|
|
8530
|
+
written.count++;
|
|
8531
|
+
}
|
|
8532
|
+
}
|
|
8533
|
+
/**
|
|
8534
|
+
* Write imported entities in batches, reporting what was committed.
|
|
8535
|
+
*
|
|
8536
|
+
* Resumable on purpose: a failure at row 4 000 leaves rows 1–3 999 in the table
|
|
8537
|
+
* whatever this does, so the only useful thing to report is where to carry on
|
|
8538
|
+
* from. `offset` is that point — the Retry button passes back the `committed`
|
|
8539
|
+
* count of the error it is retrying, instead of starting from zero and
|
|
8540
|
+
* conflicting on every row that already exists.
|
|
8541
|
+
*
|
|
8542
|
+
* @throws {ImportSaveError} naming the rows that were not written.
|
|
8543
|
+
*/
|
|
8544
|
+
async function saveImportedEntities(dataClient, path, data, { offset = 0, batchSize = 25, onBatchCommitted = () => void 0, bulkUnsupported = { current: false } } = {}) {
|
|
8545
|
+
console.debug("Saving imported data", offset, batchSize);
|
|
8546
|
+
const accessor = dataClient.collection(path);
|
|
8547
|
+
for (let start = offset; start < data.length; start += batchSize) {
|
|
8548
|
+
const batch = data.slice(start, start + batchSize);
|
|
8549
|
+
const written = { count: 0 };
|
|
8550
|
+
try {
|
|
8551
|
+
await writeBatch(accessor, batch, bulkUnsupported, written);
|
|
8552
|
+
} catch (e) {
|
|
8553
|
+
throw Object.assign(e, {
|
|
8554
|
+
committed: start + written.count,
|
|
8555
|
+
failedFrom: start + written.count,
|
|
8556
|
+
failedTo: start + batch.length
|
|
8557
|
+
});
|
|
8558
|
+
}
|
|
8559
|
+
onBatchCommitted(start + batch.length);
|
|
8560
|
+
}
|
|
8561
|
+
onBatchCommitted(data.length);
|
|
8562
|
+
}
|
|
8563
|
+
//#endregion
|
|
8099
8564
|
//#region src/data_import/components/ImportSaveInProgress.tsx
|
|
8100
8565
|
function ImportSaveInProgress({ path, importConfig, collection, onImportSuccess }) {
|
|
8101
8566
|
const [errorSaving, setErrorSaving] = useState(void 0);
|
|
8102
8567
|
const dataClient = useData();
|
|
8103
8568
|
const savingRef = useRef(false);
|
|
8569
|
+
const committedRef = useRef(0);
|
|
8570
|
+
const bulkUnsupportedRef = useRef({ current: false });
|
|
8104
8571
|
const [processedEntities, setProcessedEntities] = useState(0);
|
|
8105
8572
|
function save() {
|
|
8106
8573
|
if (savingRef.current) return;
|
|
8107
8574
|
savingRef.current = true;
|
|
8108
|
-
|
|
8575
|
+
setErrorSaving(void 0);
|
|
8576
|
+
saveImportedEntities(dataClient, path, importConfig.entities, {
|
|
8577
|
+
offset: committedRef.current,
|
|
8578
|
+
batchSize: 25,
|
|
8579
|
+
onBatchCommitted: (written) => {
|
|
8580
|
+
committedRef.current = written;
|
|
8581
|
+
setProcessedEntities(written);
|
|
8582
|
+
},
|
|
8583
|
+
bulkUnsupported: bulkUnsupportedRef.current
|
|
8584
|
+
}).then(() => {
|
|
8109
8585
|
onImportSuccess(collection);
|
|
8110
8586
|
savingRef.current = false;
|
|
8111
8587
|
}).catch((e) => {
|
|
8112
|
-
|
|
8588
|
+
const error = e;
|
|
8589
|
+
committedRef.current = error.committed ?? committedRef.current;
|
|
8590
|
+
setProcessedEntities(committedRef.current);
|
|
8591
|
+
setErrorSaving(error);
|
|
8113
8592
|
savingRef.current = false;
|
|
8114
8593
|
});
|
|
8115
8594
|
}
|
|
8116
8595
|
useEffect(() => {
|
|
8117
8596
|
save();
|
|
8118
8597
|
}, []);
|
|
8119
|
-
if (errorSaving)
|
|
8120
|
-
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
|
|
8130
|
-
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8598
|
+
if (errorSaving) {
|
|
8599
|
+
const total = importConfig.entities.length;
|
|
8600
|
+
const failedRows = errorSaving.failedTo - errorSaving.failedFrom > 1 ? `rows ${errorSaving.failedFrom + 1}–${errorSaving.failedTo}` : `row ${errorSaving.failedFrom + 1}`;
|
|
8601
|
+
return /* @__PURE__ */ jsxs(CenteredView, {
|
|
8602
|
+
className: "flex flex-col gap-4 items-center",
|
|
8603
|
+
children: [
|
|
8604
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
8605
|
+
variant: "h6",
|
|
8606
|
+
children: "Error saving data"
|
|
8607
|
+
}),
|
|
8608
|
+
/* @__PURE__ */ jsx(Typography, {
|
|
8609
|
+
variant: "body2",
|
|
8610
|
+
color: "error",
|
|
8611
|
+
children: errorSaving.message
|
|
8612
|
+
}),
|
|
8613
|
+
/* @__PURE__ */ jsxs(Typography, {
|
|
8614
|
+
variant: "body2",
|
|
8615
|
+
children: [
|
|
8616
|
+
errorSaving.committed,
|
|
8617
|
+
" of ",
|
|
8618
|
+
total,
|
|
8619
|
+
" rows were imported. The import stopped on ",
|
|
8620
|
+
failedRows,
|
|
8621
|
+
", which ",
|
|
8622
|
+
errorSaving.failedTo - errorSaving.failedFrom > 1 ? "were" : "was",
|
|
8623
|
+
" not written."
|
|
8624
|
+
]
|
|
8625
|
+
}),
|
|
8626
|
+
/* @__PURE__ */ jsx(Button, {
|
|
8627
|
+
onClick: save,
|
|
8628
|
+
children: errorSaving.committed > 0 ? `Retry from row ${errorSaving.committed + 1}` : "Retry"
|
|
8629
|
+
})
|
|
8630
|
+
]
|
|
8631
|
+
});
|
|
8632
|
+
}
|
|
8137
8633
|
return /* @__PURE__ */ jsxs("div", {
|
|
8138
8634
|
className: "flex flex-col gap-4 items-center",
|
|
8139
8635
|
children: [
|
|
@@ -8158,18 +8654,6 @@ function ImportSaveInProgress({ path, importConfig, collection, onImportSuccess
|
|
|
8158
8654
|
]
|
|
8159
8655
|
});
|
|
8160
8656
|
}
|
|
8161
|
-
function saveDataBatch(dataClient, collection, path, data, offset = 0, batchSize = 25, onProgressUpdate) {
|
|
8162
|
-
console.debug("Saving imported data", offset, batchSize);
|
|
8163
|
-
const batch = data.slice(offset, offset + batchSize);
|
|
8164
|
-
return Promise.all(batch.map((d) => dataClient.collection(path).create(d.values, d.id))).then(() => {
|
|
8165
|
-
if (offset + batchSize < data.length) {
|
|
8166
|
-
onProgressUpdate(offset + batchSize);
|
|
8167
|
-
return saveDataBatch(dataClient, collection, path, data, offset + batchSize, batchSize, onProgressUpdate);
|
|
8168
|
-
}
|
|
8169
|
-
onProgressUpdate(data.length);
|
|
8170
|
-
return Promise.resolve();
|
|
8171
|
-
});
|
|
8172
|
-
}
|
|
8173
8657
|
//#endregion
|
|
8174
8658
|
//#region src/data_import/hooks/useImportConfig.tsx
|
|
8175
8659
|
var useImportConfig = () => {
|
|
@@ -8241,6 +8725,10 @@ function convertDataToEntity(authController, navigation, data, idColumn, headers
|
|
|
8241
8725
|
}
|
|
8242
8726
|
function flattenEntry(obj, parent = "") {
|
|
8243
8727
|
return Object.keys(obj).reduce((acc, key) => {
|
|
8728
|
+
if (isPrototypePollutingKey(key)) {
|
|
8729
|
+
console.warn(`Skipping column "${key}": a header may not reach the prototype chain`);
|
|
8730
|
+
return acc;
|
|
8731
|
+
}
|
|
8244
8732
|
const prefixedKey = parent ? `${parent}.${key}` : key;
|
|
8245
8733
|
if (typeof obj[key] === "object" && !(obj[key] instanceof Date) && obj[key] !== null && !Array.isArray(obj[key])) Object.assign(acc, flattenEntry(obj[key], prefixedKey));
|
|
8246
8734
|
else acc[prefixedKey] = obj[key];
|
|
@@ -8269,8 +8757,12 @@ function processValueMapping(authController, value, navigation, property) {
|
|
|
8269
8757
|
return value;
|
|
8270
8758
|
}
|
|
8271
8759
|
if (from === "array" && to === "array" && Array.isArray(value) && usedProperty.of && !Array.isArray(usedProperty.of) && !isPropertyBuilder(usedProperty.of)) return value.map((v) => processValueMapping(authController, v, navigation, usedProperty.of));
|
|
8272
|
-
else if (from === "string" && to === "number" && typeof value === "string")
|
|
8273
|
-
|
|
8760
|
+
else if (from === "string" && to === "number" && typeof value === "string") {
|
|
8761
|
+
const trimmed = value.trim();
|
|
8762
|
+
if (trimmed === "") return null;
|
|
8763
|
+
const num = Number(trimmed);
|
|
8764
|
+
return Number.isNaN(num) ? null : num;
|
|
8765
|
+
} else if (from === "string" && to === "array" && typeof value === "string" && usedProperty.of && !Array.isArray(usedProperty.of) && !isPropertyBuilder(usedProperty.of)) return value.split(",").map((v) => processValueMapping(authController, v, navigation, usedProperty.of));
|
|
8274
8766
|
else if (from === "string" && to === "boolean") return value === "true";
|
|
8275
8767
|
else if (from === "number" && to === "boolean") return value === 1;
|
|
8276
8768
|
else if (from === "boolean" && to === "number") return value ? 1 : 0;
|
|
@@ -8311,6 +8803,7 @@ function processValueMapping(authController, value, navigation, property) {
|
|
|
8311
8803
|
//#endregion
|
|
8312
8804
|
//#region src/data_import/components/DataNewPropertiesMapping.tsx
|
|
8313
8805
|
function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPropertyView }) {
|
|
8806
|
+
const { t } = useTranslation();
|
|
8314
8807
|
const headersMapping = importConfig.headersMapping;
|
|
8315
8808
|
const headingsOrder = importConfig.headingsOrder;
|
|
8316
8809
|
const idColumn = importConfig.idColumn;
|
|
@@ -8390,7 +8883,7 @@ function DataNewPropertiesMapping({ importConfig, destinationProperties, buildPr
|
|
|
8390
8883
|
/* @__PURE__ */ jsx(TableCell, {
|
|
8391
8884
|
header: true,
|
|
8392
8885
|
style: { width: "65%" },
|
|
8393
|
-
children: "
|
|
8886
|
+
children: t("default_value")
|
|
8394
8887
|
})
|
|
8395
8888
|
] }), /* @__PURE__ */ jsx(TableBody, { children: destinationProperties && getAllPropertyKeys(destinationProperties).map((key) => {
|
|
8396
8889
|
const property = getPropertyInPath$1(destinationProperties, key);
|
|
@@ -8470,17 +8963,22 @@ function IdSelectField({ idColumn, headersMapping, onChange }) {
|
|
|
8470
8963
|
}) });
|
|
8471
8964
|
}
|
|
8472
8965
|
function DefaultValuesField({ property, onValueChange, defaultValue }) {
|
|
8966
|
+
const { t } = useTranslation();
|
|
8967
|
+
const defaultValueLabel = t("default_value");
|
|
8968
|
+
const ariaLabel = property.name ? `${defaultValueLabel}: ${property.name}` : defaultValueLabel;
|
|
8473
8969
|
if (property.type === "string") return /* @__PURE__ */ jsx(TextField, {
|
|
8474
8970
|
size: "medium",
|
|
8475
|
-
|
|
8971
|
+
"aria-label": ariaLabel,
|
|
8972
|
+
placeholder: defaultValueLabel,
|
|
8476
8973
|
value: defaultValue ?? "",
|
|
8477
8974
|
onChange: (event) => onValueChange(event.target.value)
|
|
8478
8975
|
});
|
|
8479
8976
|
else if (property.type === "number") return /* @__PURE__ */ jsx(TextField, {
|
|
8480
8977
|
size: "medium",
|
|
8481
8978
|
type: "number",
|
|
8979
|
+
"aria-label": ariaLabel,
|
|
8482
8980
|
value: defaultValue ?? "",
|
|
8483
|
-
placeholder:
|
|
8981
|
+
placeholder: defaultValueLabel,
|
|
8484
8982
|
onChange: (event) => onValueChange(event.target.value)
|
|
8485
8983
|
});
|
|
8486
8984
|
else if (property.type === "boolean") return /* @__PURE__ */ jsx(BooleanSwitchWithLabel, {
|
|
@@ -8517,6 +9015,7 @@ function ImportNewPropertyFieldPreview({ propertyKey, property, onEditClick, inc
|
|
|
8517
9015
|
className: "flex flex-row items-center gap-2",
|
|
8518
9016
|
children: [includeName && /* @__PURE__ */ jsx(TextField, {
|
|
8519
9017
|
size: "medium",
|
|
9018
|
+
"aria-label": propertyKey ? `Name of property ${propertyKey}` : "Property name",
|
|
8520
9019
|
className: "text-base grow",
|
|
8521
9020
|
value: property?.name ?? "",
|
|
8522
9021
|
onChange: (e) => {
|
|
@@ -8819,8 +9318,8 @@ function EditorCollectionAction({ path, parentCollectionSlugs, parentEntityIds,
|
|
|
8819
9318
|
}
|
|
8820
9319
|
//#endregion
|
|
8821
9320
|
//#region src/components/CollectionViewBinding/CollectionViewActions.tsx
|
|
8822
|
-
var ImportCollectionAction = lazy(() => import("./import-
|
|
8823
|
-
var ExportCollectionAction = lazy(() => import("./export-
|
|
9321
|
+
var ImportCollectionAction = lazy(() => import("./import-C_4edkoD.js").then((n) => n.t).then((m) => ({ default: m.ImportCollectionAction })));
|
|
9322
|
+
var ExportCollectionAction = lazy(() => import("./export-L53WektO.js").then((n) => n.t).then((m) => ({ default: m.ExportCollectionAction })));
|
|
8824
9323
|
function CollectionViewActions({ collection, relativePath, parentCollectionSlugs, parentEntityIds, onNewClick, onAddExistingClick, onMultipleDeleteClick, selectionEnabled, path, selectionController, tableController, collectionEntitiesCount, compact, children, openNewDocument }) {
|
|
8825
9324
|
const context = useAdminContext();
|
|
8826
9325
|
const { canCreate, canDelete } = usePermissions();
|
|
@@ -8829,14 +9328,14 @@ function CollectionViewActions({ collection, relativePath, parentCollectionSlugs
|
|
|
8829
9328
|
const collectionEditorController = useCollectionEditorController();
|
|
8830
9329
|
const hasCollectionEditor = Boolean(collectionEditorController?.editCollection);
|
|
8831
9330
|
const selectedEntities = selectionController.selectedEntities;
|
|
8832
|
-
const addButton = canCreate(collection, path) && onNewClick && (largeLayout && !compact ? /* @__PURE__ */
|
|
9331
|
+
const addButton = canCreate(collection, path) && onNewClick && (largeLayout && !compact ? /* @__PURE__ */ jsx(Button, {
|
|
8833
9332
|
id: `add_entity_${path}`,
|
|
8834
9333
|
onClick: onNewClick,
|
|
8835
9334
|
startIcon: /* @__PURE__ */ jsx(PlusIcon, { size: iconSize.small }),
|
|
8836
9335
|
size: "small",
|
|
8837
9336
|
variant: "filled",
|
|
8838
9337
|
color: "primary",
|
|
8839
|
-
children:
|
|
9338
|
+
children: t("add_specific", { name: collection.singularName ?? collection.name })
|
|
8840
9339
|
}) : /* @__PURE__ */ jsx(Button, {
|
|
8841
9340
|
id: `add_entity_${path}`,
|
|
8842
9341
|
onClick: onNewClick,
|
|
@@ -8947,46 +9446,462 @@ function CollectionViewActions({ collection, relativePath, parentCollectionSlugs
|
|
|
8947
9446
|
});
|
|
8948
9447
|
}
|
|
8949
9448
|
//#endregion
|
|
8950
|
-
//#region src/components/CollectionViewBinding/
|
|
9449
|
+
//#region src/components/CollectionViewBinding/SearchHighlight.tsx
|
|
8951
9450
|
/**
|
|
8952
|
-
*
|
|
9451
|
+
* Showing a searcher *why* a row is in their results.
|
|
8953
9452
|
*
|
|
8954
|
-
* A
|
|
9453
|
+
* A ranked list answers "which rows" and never "why this one". When the term is
|
|
9454
|
+
* in the title that is self-evident; when it is in a field the list does not
|
|
9455
|
+
* show — a certification, a nested answer, a note — the row looks arbitrary and
|
|
9456
|
+
* the only way to find out is to open it. That is the whole reason this exists.
|
|
8955
9457
|
*
|
|
8956
|
-
*
|
|
8957
|
-
* enum status keeps its colour, a date keeps its format and a storage path
|
|
8958
|
-
* becomes a thumbnail. This is why a declared path beats a resolver whenever
|
|
8959
|
-
* the value is actually on the record.
|
|
8960
|
-
* - **From a resolver** — there is no property, because the value was computed
|
|
8961
|
-
* or fetched. All that can be said about it is that it is text.
|
|
9458
|
+
* Two jobs, and they are deliberately separate:
|
|
8962
9459
|
*
|
|
8963
|
-
*
|
|
8964
|
-
*
|
|
8965
|
-
*
|
|
8966
|
-
* seven versions.
|
|
9460
|
+
* 1. {@link Highlighted} marks the term inside a value the list already shows.
|
|
9461
|
+
* 2. {@link offSlotMatch} finds a match in a field that has no slot, so the
|
|
9462
|
+
* caller can put it where the subtitle would have gone.
|
|
8967
9463
|
*/
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
9464
|
+
/** `<mark>` is Postgres's delimiter, and what `ts_headline` emits. */
|
|
9465
|
+
var MARK = /(<mark>.*?<\/mark>)/g;
|
|
9466
|
+
/**
|
|
9467
|
+
* Render a `ts_headline` snippet: text with `<mark>` around each hit.
|
|
9468
|
+
*
|
|
9469
|
+
* Split-and-render rather than `dangerouslySetInnerHTML`. The text around the
|
|
9470
|
+
* marks is whatever a user typed into their own record, and the only markup it
|
|
9471
|
+
* is allowed to carry is the highlight Postgres put there. Anything else stays
|
|
9472
|
+
* inert text.
|
|
9473
|
+
*/
|
|
9474
|
+
/**
|
|
9475
|
+
* Context kept before and after a hit, in characters.
|
|
9476
|
+
*
|
|
9477
|
+
* Asymmetric on purpose. The snippet renders on one truncated line, so anything
|
|
9478
|
+
* before the hit competes with the hit itself for the width available — with a
|
|
9479
|
+
* generous lead-in the mark is pushed off the right edge and the reader sees a
|
|
9480
|
+
* fragment with nothing highlighted in it, which is worse than no snippet at
|
|
9481
|
+
* all. Lead with just enough to not start abruptly, then run on.
|
|
9482
|
+
*/
|
|
9483
|
+
var SNIPPET_LEAD = 16;
|
|
9484
|
+
var SNIPPET_TRAIL = 90;
|
|
9485
|
+
/**
|
|
9486
|
+
* Pull the first mark near the front of the snippet.
|
|
9487
|
+
*
|
|
9488
|
+
* The snippet renders on one truncated line whose width is not knowable here —
|
|
9489
|
+
* a list column is wide, the same list beside an open record is not. Whatever
|
|
9490
|
+
* precedes the mark competes with it for that width, and when the mark loses
|
|
9491
|
+
* the reader gets a fragment of their record with nothing highlighted in it,
|
|
9492
|
+
* which reads as the feature being broken rather than as a narrow column.
|
|
9493
|
+
*
|
|
9494
|
+
* `ts_headline` centres its fragment on the match, so roughly half the window
|
|
9495
|
+
* is lead-in; that is the right shape for a paragraph and the wrong one for a
|
|
9496
|
+
* line. Trimming here rather than asking Postgres for a smaller window keeps
|
|
9497
|
+
* the trailing context, which is what actually explains the hit — and applies
|
|
9498
|
+
* the same shape to the snippets built in the browser, so a row looks the same
|
|
9499
|
+
* whichever path produced it.
|
|
9500
|
+
*/
|
|
9501
|
+
function trimSnippetLead(text, lead = SNIPPET_LEAD) {
|
|
9502
|
+
const at = text.indexOf("<mark>");
|
|
9503
|
+
if (at < 0 || at <= lead) return text;
|
|
9504
|
+
return "…" + text.slice(at - lead, at).replace(/^\S*\s/, "") + text.slice(at);
|
|
9505
|
+
}
|
|
9506
|
+
function Snippet({ text, className }) {
|
|
9507
|
+
return /* @__PURE__ */ jsx("span", {
|
|
9508
|
+
className,
|
|
9509
|
+
children: useMemo(() => trimSnippetLead(text).split(MARK), [text]).map((part, i) => part.startsWith("<mark>") ? /* @__PURE__ */ jsx("mark", {
|
|
9510
|
+
className: "bg-amber-200/70 dark:bg-amber-400/30 text-inherit rounded-[3px] px-[1px]",
|
|
9511
|
+
children: part.slice(6, -7)
|
|
9512
|
+
}, i) : /* @__PURE__ */ jsx(React.Fragment, { children: part }, i))
|
|
8978
9513
|
});
|
|
8979
9514
|
}
|
|
8980
|
-
|
|
8981
|
-
|
|
9515
|
+
/**
|
|
9516
|
+
* Mark every occurrence of the searched terms inside a plain value.
|
|
9517
|
+
*
|
|
9518
|
+
* Used for values the list already renders, where the server's snippet would be
|
|
9519
|
+
* the wrong thing to show: the snippet is truncated to a fragment and folded to
|
|
9520
|
+
* the indexed form, while the cell should keep the record's real text. So the
|
|
9521
|
+
* terms are re-matched here, on the display value.
|
|
9522
|
+
*
|
|
9523
|
+
* Deliberately dumber than the server's matching, and it has to be: this cannot
|
|
9524
|
+
* stem or unaccent without reimplementing the text search configuration in the
|
|
9525
|
+
* browser and getting it subtly different. It marks what it can find as a
|
|
9526
|
+
* prefix, case- and accent-insensitively, and marks nothing when it cannot —
|
|
9527
|
+
* a highlight that is merely absent reads as "no hit in this field", which is
|
|
9528
|
+
* the honest answer for a stem the browser cannot reproduce.
|
|
9529
|
+
*/
|
|
9530
|
+
function Highlighted({ text, terms, className }) {
|
|
9531
|
+
const segments = useMemo(() => splitOnTerms(text, terms), [text, terms]);
|
|
9532
|
+
if (segments.length <= 1) return /* @__PURE__ */ jsx("span", {
|
|
9533
|
+
className,
|
|
9534
|
+
children: text
|
|
9535
|
+
});
|
|
9536
|
+
return /* @__PURE__ */ jsx("span", {
|
|
9537
|
+
className,
|
|
9538
|
+
children: segments.map((seg, i) => seg.hit ? /* @__PURE__ */ jsx("mark", {
|
|
9539
|
+
className: "bg-amber-200/70 dark:bg-amber-400/30 text-inherit rounded-[3px] px-[1px]",
|
|
9540
|
+
children: seg.text
|
|
9541
|
+
}, i) : /* @__PURE__ */ jsx(React.Fragment, { children: seg.text }, i))
|
|
9542
|
+
});
|
|
9543
|
+
}
|
|
9544
|
+
/** Strip diacritics so `gestion` marks `gestión`, mirroring the index's unaccent. */
|
|
9545
|
+
var fold = (value) => value.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
9546
|
+
/**
|
|
9547
|
+
* Split `text` into hit / non-hit runs.
|
|
9548
|
+
*
|
|
9549
|
+
* Matches on the *folded* form but slices the original, so the marked text
|
|
9550
|
+
* keeps its accents and case — `Gestión`, not `gestion`. Folding is
|
|
9551
|
+
* length-preserving for the diacritics this handles, which is what makes the
|
|
9552
|
+
* indices transferable; a fold that changed length would misalign the slices.
|
|
9553
|
+
*/
|
|
9554
|
+
function splitOnTerms(text, terms) {
|
|
9555
|
+
const haystack = fold(text);
|
|
9556
|
+
const needles = terms.map(fold).filter((t) => t.length >= 2);
|
|
9557
|
+
if (needles.length === 0) return [{
|
|
9558
|
+
text,
|
|
9559
|
+
hit: false
|
|
9560
|
+
}];
|
|
9561
|
+
const bounds = [];
|
|
9562
|
+
for (const needle of needles) {
|
|
9563
|
+
let from = 0;
|
|
9564
|
+
for (;;) {
|
|
9565
|
+
const at = haystack.indexOf(needle, from);
|
|
9566
|
+
if (at === -1) break;
|
|
9567
|
+
const before = at === 0 ? " " : haystack[at - 1];
|
|
9568
|
+
if (!/[\p{L}\p{N}]/u.test(before)) bounds.push([at, at + needle.length]);
|
|
9569
|
+
from = at + needle.length;
|
|
9570
|
+
}
|
|
9571
|
+
}
|
|
9572
|
+
if (bounds.length === 0) return [{
|
|
9573
|
+
text,
|
|
9574
|
+
hit: false
|
|
9575
|
+
}];
|
|
9576
|
+
bounds.sort((a, b) => a[0] - b[0]);
|
|
9577
|
+
const merged = [];
|
|
9578
|
+
for (const [start, end] of bounds) {
|
|
9579
|
+
const last = merged[merged.length - 1];
|
|
9580
|
+
if (last && start <= last[1]) last[1] = Math.max(last[1], end);
|
|
9581
|
+
else merged.push([start, end]);
|
|
9582
|
+
}
|
|
9583
|
+
const segments = [];
|
|
9584
|
+
let cursor = 0;
|
|
9585
|
+
for (const [start, end] of merged) {
|
|
9586
|
+
if (start > cursor) segments.push({
|
|
9587
|
+
text: text.slice(cursor, start),
|
|
9588
|
+
hit: false
|
|
9589
|
+
});
|
|
9590
|
+
segments.push({
|
|
9591
|
+
text: text.slice(start, end),
|
|
9592
|
+
hit: true
|
|
9593
|
+
});
|
|
9594
|
+
cursor = end;
|
|
9595
|
+
}
|
|
9596
|
+
if (cursor < text.length) segments.push({
|
|
9597
|
+
text: text.slice(cursor),
|
|
9598
|
+
hit: false
|
|
9599
|
+
});
|
|
9600
|
+
return segments;
|
|
9601
|
+
}
|
|
9602
|
+
/**
|
|
9603
|
+
* The terms a searcher typed, as things to look for in a rendered value.
|
|
9604
|
+
*
|
|
9605
|
+
* Quoted phrases stay whole; `-excluded` terms are dropped, since marking a
|
|
9606
|
+
* word the user asked *not* to see would be actively wrong. `websearch_to_tsquery`
|
|
9607
|
+
* reads the same syntax on the server.
|
|
9608
|
+
*/
|
|
9609
|
+
function searchTerms(searchString) {
|
|
9610
|
+
if (!searchString) return [];
|
|
9611
|
+
const terms = [];
|
|
9612
|
+
for (const [, phrase, bare] of searchString.matchAll(/"([^"]+)"|(\S+)/g)) {
|
|
9613
|
+
const term = phrase ?? bare;
|
|
9614
|
+
if (!term || term.startsWith("-")) continue;
|
|
9615
|
+
if (/^(or|and)$/i.test(term)) continue;
|
|
9616
|
+
terms.push(term);
|
|
9617
|
+
}
|
|
9618
|
+
return terms;
|
|
9619
|
+
}
|
|
9620
|
+
/**
|
|
9621
|
+
* The best match to show when the term is nowhere the list already displays.
|
|
9622
|
+
*
|
|
9623
|
+
* `shownKeys` are the property paths the row is already rendering, so a match
|
|
9624
|
+
* there needs no explaining — {@link Highlighted} marks it in place and the
|
|
9625
|
+
* subtitle keeps its own job. Anything else is invisible, and one of those is
|
|
9626
|
+
* what the subtitle slot gets replaced with.
|
|
9627
|
+
*
|
|
9628
|
+
* The first such match wins, and the server returns them in the order the
|
|
9629
|
+
* collection declared its `search` fields — so the field an author put first is
|
|
9630
|
+
* the one a reader sees, rather than whichever the database aggregated first.
|
|
9631
|
+
*/
|
|
9632
|
+
function offSlotMatch(matches, shownKeys) {
|
|
9633
|
+
if (!matches || matches.length === 0) return void 0;
|
|
9634
|
+
const shown = new Set(shownKeys.filter((k) => Boolean(k)));
|
|
9635
|
+
return matches.find((m) => !shown.has(m.field));
|
|
9636
|
+
}
|
|
9637
|
+
/**
|
|
9638
|
+
* Where a search term appears in a row, computed from the row itself.
|
|
9639
|
+
*
|
|
9640
|
+
* The server can say this too — `_matches`, from `ts_headline` — and its answer
|
|
9641
|
+
* is better, because it knows the stemming the index used. This exists because
|
|
9642
|
+
* the admin cannot rely on getting it: rows reach the panel over three
|
|
9643
|
+
* different transports (REST, the realtime socket, the offline cache) and only
|
|
9644
|
+
* one of them carries query metadata. A feature that explains a result on some
|
|
9645
|
+
* loads and not others is worse than one that always gives a slightly simpler
|
|
9646
|
+
* answer.
|
|
9647
|
+
*
|
|
9648
|
+
* So: use the server's when it is there, fall back to this. Both produce the
|
|
9649
|
+
* same `{ field, snippet }` shape, and this one marks with the same `<mark>`.
|
|
9650
|
+
*/
|
|
9651
|
+
function localRowMatch(values, properties, terms, shownKeys) {
|
|
9652
|
+
if (!values || !properties || terms.length === 0) return void 0;
|
|
9653
|
+
const shown = new Set(shownKeys.filter((k) => Boolean(k)));
|
|
9654
|
+
for (const [key, property] of Object.entries(properties)) {
|
|
9655
|
+
if (shown.has(key)) continue;
|
|
9656
|
+
const found = findInValue(values[key], terms, key, property);
|
|
9657
|
+
if (found) return found;
|
|
9658
|
+
}
|
|
9659
|
+
}
|
|
9660
|
+
function findInValue(value, terms, path, property) {
|
|
9661
|
+
if (property && "enum" in property && property.enum) return void 0;
|
|
9662
|
+
if (typeof value === "string") {
|
|
9663
|
+
const snippet = snippetAround(value, terms);
|
|
9664
|
+
return snippet ? {
|
|
9665
|
+
field: path,
|
|
9666
|
+
snippet
|
|
9667
|
+
} : void 0;
|
|
9668
|
+
}
|
|
9669
|
+
if (Array.isArray(value)) {
|
|
9670
|
+
for (const item of value) {
|
|
9671
|
+
const found = findInValue(item, terms, path, void 0);
|
|
9672
|
+
if (found) return found;
|
|
9673
|
+
}
|
|
9674
|
+
return;
|
|
9675
|
+
}
|
|
9676
|
+
if (value && typeof value === "object") for (const [key, nested] of Object.entries(value)) {
|
|
9677
|
+
const found = findInValue(nested, terms, `${path}.${key}`, void 0);
|
|
9678
|
+
if (found) return found;
|
|
9679
|
+
}
|
|
9680
|
+
}
|
|
9681
|
+
/**
|
|
9682
|
+
* The text around the first hit, with every hit in view marked.
|
|
9683
|
+
*
|
|
9684
|
+
* Trimmed to a readable window rather than the whole field: a match in a long
|
|
9685
|
+
* body should show the sentence it is in, not the first line of the document,
|
|
9686
|
+
* and certainly not the document.
|
|
9687
|
+
*/
|
|
9688
|
+
function snippetAround(text, terms) {
|
|
9689
|
+
const segments = splitOnTerms(text, terms);
|
|
9690
|
+
const firstHit = segments.findIndex((s) => s.hit);
|
|
9691
|
+
if (firstHit === -1) return void 0;
|
|
9692
|
+
const before = segments.slice(0, firstHit).map((s) => s.text).join("");
|
|
9693
|
+
const head = before.length > SNIPPET_LEAD ? "…" + before.slice(before.length - SNIPPET_LEAD).replace(/^\S*\s/, "") : before;
|
|
9694
|
+
let out = head;
|
|
9695
|
+
for (let i = firstHit; i < segments.length; i++) {
|
|
9696
|
+
const seg = segments[i];
|
|
9697
|
+
out += seg.hit ? `<mark>${seg.text}</mark>` : seg.text;
|
|
9698
|
+
if (out.length > head.length + SNIPPET_TRAIL) {
|
|
9699
|
+
out += "…";
|
|
9700
|
+
break;
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
return out.trim();
|
|
9704
|
+
}
|
|
9705
|
+
//#endregion
|
|
9706
|
+
//#region src/components/CollectionViewBinding/SearchExplanation.tsx
|
|
9707
|
+
/**
|
|
9708
|
+
* The one place a view asks "why is this row here?" and gets something to draw.
|
|
9709
|
+
*
|
|
9710
|
+
* List, cards and board all render the same title-and-subtitle slots over the
|
|
9711
|
+
* same machinery. An explanation that shows up in one of them and not the others
|
|
9712
|
+
* reads as randomness rather than as a feature, so the decision and the markup
|
|
9713
|
+
* live here and each view calls them.
|
|
9714
|
+
*/
|
|
9715
|
+
/**
|
|
9716
|
+
* Terms to mark inside values the row already shows, plus the single match
|
|
9717
|
+
* worth putting where the subtitle would go — or nothing, when the row is not
|
|
9718
|
+
* the product of a search or the hit is already visible.
|
|
9719
|
+
*
|
|
9720
|
+
* Prefers the server's `_matches`, which knows the stemming the index used, and
|
|
9721
|
+
* falls back to reading the row directly. That fallback is not belt-and-braces:
|
|
9722
|
+
* rows reach the panel over REST, the realtime socket and the offline cache,
|
|
9723
|
+
* and only one of those carries query metadata.
|
|
9724
|
+
*/
|
|
9725
|
+
function useSearchExplanation(entity, properties, shownKeys, searchString) {
|
|
9726
|
+
const terms = useMemo(() => searchTerms(searchString), [searchString]);
|
|
9727
|
+
const shownKey = shownKeys.map((k) => k ?? "").join("|");
|
|
9728
|
+
return {
|
|
9729
|
+
terms,
|
|
9730
|
+
offSlot: useMemo(() => {
|
|
9731
|
+
if (terms.length === 0) return void 0;
|
|
9732
|
+
const shown = shownKey.split("|").map((k) => k || void 0);
|
|
9733
|
+
return offSlotMatch(entity.searchMatches, shown) ?? localRowMatch(entity.values, properties, terms, shown);
|
|
9734
|
+
}, [
|
|
9735
|
+
entity.searchMatches,
|
|
9736
|
+
entity.values,
|
|
9737
|
+
properties,
|
|
9738
|
+
shownKey,
|
|
9739
|
+
terms
|
|
9740
|
+
])
|
|
9741
|
+
};
|
|
9742
|
+
}
|
|
9743
|
+
/**
|
|
9744
|
+
* Which field the hit was in, and the text around it.
|
|
9745
|
+
*
|
|
9746
|
+
* Sits where the subtitle would have gone. Deliberately quiet — a small amber
|
|
9747
|
+
* caption and one line — because it is context for a result, not the result. It
|
|
9748
|
+
* has to survive a narrow column, so it truncates rather than wraps.
|
|
9749
|
+
*/
|
|
9750
|
+
function MatchExplanation({ match, label }) {
|
|
9751
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
9752
|
+
className: "mt-0.5 flex items-baseline gap-1.5 min-w-0",
|
|
9753
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
9754
|
+
className: "shrink-0 text-[10px] uppercase tracking-wide font-semibold text-amber-700 dark:text-amber-500/90",
|
|
9755
|
+
title: match.field,
|
|
9756
|
+
children: label
|
|
9757
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
9758
|
+
className: "truncate text-xs text-surface-500 dark:text-surface-400 min-w-0",
|
|
9759
|
+
children: /* @__PURE__ */ jsx(Snippet, { text: match.snippet })
|
|
9760
|
+
})]
|
|
9761
|
+
});
|
|
9762
|
+
}
|
|
9763
|
+
/**
|
|
9764
|
+
* What to call a matched field on screen.
|
|
9765
|
+
*
|
|
9766
|
+
* The path is what the config and the server speak — `questionnaire.
|
|
9767
|
+
* certifications` — and is unreadable in a list. Resolve it to the property's
|
|
9768
|
+
* declared `name`, walking into `map` properties for a dotted path, and fall
|
|
9769
|
+
* back to the last segment humanised, so an unnamed field still reads as words
|
|
9770
|
+
* rather than as a path.
|
|
9771
|
+
*/
|
|
9772
|
+
function fieldLabel(properties, path) {
|
|
9773
|
+
const segments = path.split(".");
|
|
9774
|
+
let node = properties;
|
|
9775
|
+
let named;
|
|
9776
|
+
for (const segment of segments) {
|
|
9777
|
+
const property = node?.[segment];
|
|
9778
|
+
if (!property) break;
|
|
9779
|
+
named = property.name ?? named;
|
|
9780
|
+
node = property.properties;
|
|
9781
|
+
}
|
|
9782
|
+
if (named) return named;
|
|
9783
|
+
return (segments[segments.length - 1] ?? path).replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
9784
|
+
}
|
|
9785
|
+
//#endregion
|
|
9786
|
+
//#region src/components/CollectionViewBinding/SlotValue.tsx
|
|
9787
|
+
/**
|
|
9788
|
+
* One display slot, rendered.
|
|
9789
|
+
*
|
|
9790
|
+
* A slot's value arrives one of two ways and they cannot be rendered the same:
|
|
9791
|
+
*
|
|
9792
|
+
* - **From a property** — render it with that property's own preview, so an
|
|
9793
|
+
* enum status keeps its colour, a date keeps its format and a storage path
|
|
9794
|
+
* becomes a thumbnail. This is why a declared path beats a resolver whenever
|
|
9795
|
+
* the value is actually on the record.
|
|
9796
|
+
* - **From a resolver** — there is no property, because the value was computed
|
|
9797
|
+
* or fetched. All that can be said about it is that it is text.
|
|
9798
|
+
*
|
|
9799
|
+
* The branch lives here, once, rather than at each of the fourteen places the
|
|
9800
|
+
* list row, the card and the board card render a slot. That count is the reason:
|
|
9801
|
+
* the last time a rule like this was inlined per call site, it drifted into
|
|
9802
|
+
* seven versions.
|
|
9803
|
+
*/
|
|
9804
|
+
function SlotValue({ slot, size, textOnly = true, fill }) {
|
|
9805
|
+
if (!slot || slot.value === void 0 || slot.value === null) return null;
|
|
9806
|
+
if (!slot.property || !slot.propertyKey) {
|
|
9807
|
+
if (slot.role === "image" && typeof slot.value === "string" && slot.value.trim()) return /* @__PURE__ */ jsx(ComputedImage, {
|
|
9808
|
+
value: slot.value.trim(),
|
|
9809
|
+
size,
|
|
9810
|
+
fill
|
|
9811
|
+
});
|
|
9812
|
+
return /* @__PURE__ */ jsx(Fragment, { children: Array.isArray(slot.value) ? slot.value.map((item) => String(item)).join(", ") : String(slot.value) });
|
|
9813
|
+
}
|
|
9814
|
+
return /* @__PURE__ */ jsx(PropertyPreview, {
|
|
9815
|
+
propertyKey: slot.propertyKey,
|
|
9816
|
+
value: slot.value,
|
|
9817
|
+
property: slot.property,
|
|
9818
|
+
size,
|
|
9819
|
+
textOnly,
|
|
9820
|
+
fill
|
|
9821
|
+
});
|
|
9822
|
+
}
|
|
9823
|
+
/**
|
|
9824
|
+
* The `tags` slot, as chips beside the status.
|
|
9825
|
+
*
|
|
9826
|
+
* Honours both arms the same way the rest of the file does. A declared **path**
|
|
9827
|
+
* goes through {@link SlotValue}, so an array of enum values keeps its own
|
|
9828
|
+
* colours — that is the stated advantage of naming a property. A **resolver**
|
|
9829
|
+
* returns bare strings with no property to render them as, so they become plain
|
|
9830
|
+
* chips; a single string is accepted as one tag, which the role's type documents.
|
|
9831
|
+
*
|
|
9832
|
+
* Lives here rather than in the list, the card and the board card because that is
|
|
9833
|
+
* three copies of the same decision, and this file exists to hold exactly one.
|
|
9834
|
+
*/
|
|
9835
|
+
function TagChips({ slot, max = 4 }) {
|
|
9836
|
+
if (slot.property && slot.propertyKey) return /* @__PURE__ */ jsx(SlotValue, {
|
|
9837
|
+
slot,
|
|
9838
|
+
size: "small"
|
|
9839
|
+
});
|
|
9840
|
+
const values = (Array.isArray(slot.value) ? slot.value : [slot.value]).filter((item) => item !== void 0 && item !== null && String(item).trim() !== "").map((item) => String(item));
|
|
9841
|
+
if (values.length === 0) return null;
|
|
9842
|
+
const shown = values.slice(0, max);
|
|
9843
|
+
const hidden = values.length - shown.length;
|
|
9844
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [shown.map((value) => /* @__PURE__ */ jsx(Chip, {
|
|
9845
|
+
size: "smallest",
|
|
9846
|
+
className: "!text-[10px] !leading-tight !py-0 shrink-0 max-w-[100px] truncate",
|
|
9847
|
+
children: value
|
|
9848
|
+
}, value)), hidden > 0 && /* @__PURE__ */ jsxs("span", {
|
|
9849
|
+
className: "text-[10px] text-surface-400 dark:text-surface-500 shrink-0",
|
|
9850
|
+
children: ["+", hidden]
|
|
9851
|
+
})] });
|
|
9852
|
+
}
|
|
9853
|
+
/**
|
|
9854
|
+
* Whether a computed image string can be fetched as-is, or has to be resolved
|
|
9855
|
+
* through storage first.
|
|
9856
|
+
*
|
|
9857
|
+
* Exported because it is the whole decision: rendering either branch needs a
|
|
9858
|
+
* provider (a storage source, a DOM), and the thing worth pinning is which
|
|
9859
|
+
* branch a given string takes. `//host/x` is protocol-relative and equally
|
|
9860
|
+
* fetchable; `data:` and `blob:` are already the bytes.
|
|
9861
|
+
*/
|
|
9862
|
+
function isFetchableImageUrl(value) {
|
|
9863
|
+
return /^(https?:)?\/\//i.test(value) || /^(data|blob):/i.test(value);
|
|
9864
|
+
}
|
|
9865
|
+
/**
|
|
9866
|
+
* A string a resolver returned for the `image` role, rendered as a picture.
|
|
9867
|
+
*
|
|
9868
|
+
* Two things can arrive here and they need different handling, which is the whole
|
|
9869
|
+
* reason a property-path image is preferable: a storage property knows which
|
|
9870
|
+
* backend holds it, and a bare string does not.
|
|
9871
|
+
*
|
|
9872
|
+
* - An **absolute URL** is already fetchable. Rendering it through the storage
|
|
9873
|
+
* layer would ask a bucket to sign a path that was never in it.
|
|
9874
|
+
* - Anything else is read as a **storage path** on the default source, which is
|
|
9875
|
+
* the only source a value with no property can be attributed to. A resolver
|
|
9876
|
+
* pointing at a named source should name the property instead.
|
|
9877
|
+
*/
|
|
9878
|
+
function ComputedImage({ value, size, fill }) {
|
|
9879
|
+
if (isFetchableImageUrl(value)) return /* @__PURE__ */ jsx(UrlComponentPreview, {
|
|
9880
|
+
previewType: "image",
|
|
9881
|
+
url: value,
|
|
9882
|
+
size,
|
|
9883
|
+
fill,
|
|
9884
|
+
hint: value
|
|
9885
|
+
});
|
|
9886
|
+
return /* @__PURE__ */ jsx(StorageThumbnail, {
|
|
9887
|
+
storagePathOrDownloadUrl: value,
|
|
9888
|
+
storeUrl: false,
|
|
9889
|
+
size,
|
|
9890
|
+
fill
|
|
9891
|
+
});
|
|
9892
|
+
}
|
|
9893
|
+
//#endregion
|
|
9894
|
+
//#region src/components/CollectionViewBinding/EntityCardBinding.tsx
|
|
8982
9895
|
/**
|
|
8983
9896
|
* Card component for displaying a entity in a grid view.
|
|
8984
9897
|
* Shows thumbnail, title, and preview properties via the shared slot system.
|
|
8985
9898
|
*/
|
|
8986
|
-
function EntityCardBinding({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, size = "m" }) {
|
|
9899
|
+
function EntityCardBinding({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, size = "m", searchString }) {
|
|
8987
9900
|
const authController = useAuthController();
|
|
8988
9901
|
const analyticsController = useAnalyticsController();
|
|
8989
|
-
const
|
|
9902
|
+
const slotKeys = useCollectionSlotKeys(collection, authController, useCustomizationController().propertyConfigs);
|
|
9903
|
+
const slots = useEntitySlots(entity, collection, slotKeys);
|
|
9904
|
+
const { terms, offSlot } = useSearchExplanation(entity, collection.properties, [slotKeys.titleKey, slotKeys.subtitleKey], searchString);
|
|
8990
9905
|
const handleClick = (e) => {
|
|
8991
9906
|
if (e && (e.metaKey || e.ctrlKey) && selectionEnabled) {
|
|
8992
9907
|
e.preventDefault();
|
|
@@ -9049,7 +9964,10 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
|
|
|
9049
9964
|
}),
|
|
9050
9965
|
/* @__PURE__ */ jsx("div", {
|
|
9051
9966
|
className: "truncate my-1 text-sm font-medium min-h-[20px]",
|
|
9052
|
-
children: slots.title ? /* @__PURE__ */ jsx(
|
|
9967
|
+
children: slots.title ? terms.length > 0 && typeof slots.title.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
|
|
9968
|
+
text: slots.title.value,
|
|
9969
|
+
terms
|
|
9970
|
+
}) : /* @__PURE__ */ jsx(SlotValue, {
|
|
9053
9971
|
slot: slots.title,
|
|
9054
9972
|
size: "small"
|
|
9055
9973
|
}) : /* @__PURE__ */ jsx(Typography, {
|
|
@@ -9058,9 +9976,15 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
|
|
|
9058
9976
|
children: entity.id
|
|
9059
9977
|
})
|
|
9060
9978
|
}),
|
|
9061
|
-
|
|
9979
|
+
offSlot ? /* @__PURE__ */ jsx(MatchExplanation, {
|
|
9980
|
+
match: offSlot,
|
|
9981
|
+
label: fieldLabel(collection.properties, offSlot.field)
|
|
9982
|
+
}) : slots.subtitle && /* @__PURE__ */ jsx("div", {
|
|
9062
9983
|
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",
|
|
9063
|
-
children: /* @__PURE__ */ jsx(
|
|
9984
|
+
children: terms.length > 0 && typeof slots.subtitle.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
|
|
9985
|
+
text: slots.subtitle.value,
|
|
9986
|
+
terms
|
|
9987
|
+
}) : /* @__PURE__ */ jsx(SlotValue, {
|
|
9064
9988
|
slot: slots.subtitle,
|
|
9065
9989
|
size: "small"
|
|
9066
9990
|
})
|
|
@@ -9077,12 +10001,16 @@ function EntityCardBinding({ entity, collection, onClick, selected, highlighted,
|
|
|
9077
10001
|
children: ["+", slots.relations.reduce((acc, r) => acc + Math.max(0, r.totalCount - r.items.length), 0)]
|
|
9078
10002
|
})]
|
|
9079
10003
|
}),
|
|
9080
|
-
slots.status && /* @__PURE__ */
|
|
9081
|
-
className: "mt-1",
|
|
9082
|
-
children: /* @__PURE__ */ jsx(SlotValue, {
|
|
10004
|
+
(slots.status || slots.tags) && /* @__PURE__ */ jsxs("div", {
|
|
10005
|
+
className: "flex items-center gap-1 mt-1 flex-wrap",
|
|
10006
|
+
children: [slots.status && /* @__PURE__ */ jsx(SlotValue, {
|
|
9083
10007
|
slot: slots.status,
|
|
9084
10008
|
size: "small"
|
|
9085
|
-
})
|
|
10009
|
+
}), slots.tags && /* @__PURE__ */ jsx(TagChips, { slot: slots.tags })]
|
|
10010
|
+
}),
|
|
10011
|
+
slots.date && /* @__PURE__ */ jsx("div", {
|
|
10012
|
+
className: "mt-1 text-[10px] text-surface-500 dark:text-surface-400",
|
|
10013
|
+
children: slots.date.formatted
|
|
9086
10014
|
})
|
|
9087
10015
|
]
|
|
9088
10016
|
})]
|
|
@@ -9129,6 +10057,7 @@ function CollectionCardViewBinding({ collection, tableController, onEntityClick,
|
|
|
9129
10057
|
renderCard: useCallback((entity, { selected, highlighted, onClick }) => /* @__PURE__ */ jsx(ResolvedEntityCard, {
|
|
9130
10058
|
entity,
|
|
9131
10059
|
collection,
|
|
10060
|
+
searchString: tableController.searchString,
|
|
9132
10061
|
onClick,
|
|
9133
10062
|
selected,
|
|
9134
10063
|
highlighted,
|
|
@@ -9140,12 +10069,51 @@ function CollectionCardViewBinding({ collection, tableController, onEntityClick,
|
|
|
9140
10069
|
selectionEnabled,
|
|
9141
10070
|
size,
|
|
9142
10071
|
handleRowSelectionChange,
|
|
9143
|
-
ResolvedEntityCard
|
|
10072
|
+
ResolvedEntityCard,
|
|
10073
|
+
tableController.searchString
|
|
9144
10074
|
])
|
|
9145
10075
|
});
|
|
9146
10076
|
}
|
|
9147
10077
|
//#endregion
|
|
9148
10078
|
//#region src/components/CollectionViewBinding/CollectionListViewBinding.tsx
|
|
10079
|
+
/** A column left over from a sort or filter that is no longer applied. */
|
|
10080
|
+
var PRIORITY_STALE_REQUEST = 1e3;
|
|
10081
|
+
/** The collection's own columns. Slots sit just above it, in give-way order. */
|
|
10082
|
+
var PRIORITY_DECLARED = 2e3;
|
|
10083
|
+
var PRIORITY_DATE = PRIORITY_DECLARED;
|
|
10084
|
+
var PRIORITY_STATUS = 2001;
|
|
10085
|
+
var PRIORITY_TAGS = 2002;
|
|
10086
|
+
/** The sort or filter currently in force. */
|
|
10087
|
+
var PRIORITY_ACTIVE_REQUEST = 3e3;
|
|
10088
|
+
/**
|
|
10089
|
+
* How wide a column of this property wants to be — the Tailwind class the cell
|
|
10090
|
+
* gets, and the pixel cost of that class. The two are returned together because
|
|
10091
|
+
* they must not drift: the budget is only meaningful if it describes the widths
|
|
10092
|
+
* actually rendered. `lg:` variants key off the viewport, not this panel, so a
|
|
10093
|
+
* narrow split on a wide screen gets the larger of the two — the cost we budget.
|
|
10094
|
+
*/
|
|
10095
|
+
function getIdealColumnWidth(prop) {
|
|
10096
|
+
if (prop.type === "string" && "enum" in prop) return {
|
|
10097
|
+
width: "flex-shrink-0 w-32",
|
|
10098
|
+
widthPx: 128
|
|
10099
|
+
};
|
|
10100
|
+
if (prop.type === "date" || prop.type === "number" || prop.type === "boolean") return {
|
|
10101
|
+
width: "flex-shrink-0 w-28",
|
|
10102
|
+
widthPx: 112
|
|
10103
|
+
};
|
|
10104
|
+
if (prop.type === "reference" || prop.type === "relation") return {
|
|
10105
|
+
width: "flex-shrink-0 w-56 lg:w-64",
|
|
10106
|
+
widthPx: 256
|
|
10107
|
+
};
|
|
10108
|
+
if (prop.type === "string") return {
|
|
10109
|
+
width: "flex-shrink-0 w-40 lg:w-48",
|
|
10110
|
+
widthPx: 192
|
|
10111
|
+
};
|
|
10112
|
+
return {
|
|
10113
|
+
width: "flex-shrink-0 w-40",
|
|
10114
|
+
widthPx: 160
|
|
10115
|
+
};
|
|
10116
|
+
}
|
|
9149
10117
|
/**
|
|
9150
10118
|
* Get row padding/spacing classes based on size
|
|
9151
10119
|
*/
|
|
@@ -9159,6 +10127,31 @@ function getRowClasses(size) {
|
|
|
9159
10127
|
default: return "py-3 px-5";
|
|
9160
10128
|
}
|
|
9161
10129
|
}
|
|
10130
|
+
/** `px-5` on both sides. */
|
|
10131
|
+
var ROW_PADDING_WIDTH = 40;
|
|
10132
|
+
/** `gap-4` between every cell of the row. */
|
|
10133
|
+
var COLUMN_GAP = 16;
|
|
10134
|
+
/**
|
|
10135
|
+
* The title's share of a row, taken before any column bids for width.
|
|
10136
|
+
*
|
|
10137
|
+
* A title is a name, and a name truncated to "Lider Sr. en…" has stopped being
|
|
10138
|
+
* one. Columns are a scanner's convenience; the title is the row's identity, so
|
|
10139
|
+
* a narrow list spends its width on the title and simply shows fewer columns.
|
|
10140
|
+
*/
|
|
10141
|
+
var TITLE_COMFORTABLE_WIDTH = 320;
|
|
10142
|
+
/** An `IconButton size="small"` (`w-8`), which is what a row action renders as. */
|
|
10143
|
+
var ACTION_BUTTON_WIDTH = 32;
|
|
10144
|
+
/** `gap-0.5` between row actions. */
|
|
10145
|
+
var ACTION_BUTTON_GAP = 2;
|
|
10146
|
+
/**
|
|
10147
|
+
* How many rows are read to size the actions cell.
|
|
10148
|
+
*
|
|
10149
|
+
* Actions are resolved per record, so the width they need is only knowable by
|
|
10150
|
+
* asking — and asking every loaded row would mean re-resolving thousands of
|
|
10151
|
+
* them on every page. The first rows are representative: a collection whose
|
|
10152
|
+
* actions vary at all varies within its first screenful.
|
|
10153
|
+
*/
|
|
10154
|
+
var ACTIONS_SAMPLE_SIZE = 20;
|
|
9162
10155
|
/** Stable empty array for when no list-view actions are available. */
|
|
9163
10156
|
var EMPTY_LIST_VIEW_ACTIONS = [];
|
|
9164
10157
|
/**
|
|
@@ -9237,26 +10230,33 @@ function compactValueSummary(value, property) {
|
|
|
9237
10230
|
return null;
|
|
9238
10231
|
}
|
|
9239
10232
|
/**
|
|
9240
|
-
* Format a date value for display in the list
|
|
10233
|
+
* Format a date value for display in the list.
|
|
10234
|
+
*
|
|
10235
|
+
* A date column holds whatever the developer put in it, so the value is as
|
|
10236
|
+
* likely to be ahead of now as behind it. The relative phrase therefore comes
|
|
10237
|
+
* from {@link formatRelativeTime}, which reads the sign: a row due this
|
|
10238
|
+
* afternoon says "in 2h" rather than the "-1d ago" that flooring a negative
|
|
10239
|
+
* difference used to produce.
|
|
9241
10240
|
*/
|
|
9242
|
-
function formatDateValue(value) {
|
|
10241
|
+
function formatDateValue(value, now = /* @__PURE__ */ new Date()) {
|
|
9243
10242
|
if (!value) return null;
|
|
9244
10243
|
let date = null;
|
|
9245
10244
|
if (value instanceof Date) date = value;
|
|
9246
10245
|
else if (typeof value === "string" || typeof value === "number") date = new Date(value);
|
|
9247
10246
|
if (!date || isNaN(date.getTime())) return null;
|
|
9248
|
-
const diffMs =
|
|
10247
|
+
const diffMs = now.getTime() - date.getTime();
|
|
9249
10248
|
const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
|
|
9250
10249
|
if (diffDays === 0) return date.toLocaleTimeString(void 0, {
|
|
9251
10250
|
hour: "2-digit",
|
|
9252
10251
|
minute: "2-digit"
|
|
9253
10252
|
});
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
10253
|
+
if (diffDays === 1) return "Yesterday";
|
|
10254
|
+
const relative = formatRelativeTime(date, { now });
|
|
10255
|
+
if (relative) return relative;
|
|
10256
|
+
return date.toLocaleDateString(void 0, {
|
|
9257
10257
|
month: "short",
|
|
9258
10258
|
day: "numeric",
|
|
9259
|
-
year:
|
|
10259
|
+
year: date.getFullYear() !== now.getFullYear() ? "numeric" : void 0
|
|
9260
10260
|
});
|
|
9261
10261
|
}
|
|
9262
10262
|
/**
|
|
@@ -9272,148 +10272,270 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
|
|
|
9272
10272
|
const analyticsController = useAnalyticsController();
|
|
9273
10273
|
const context = useAdminContext();
|
|
9274
10274
|
const containerRef = useRef(null);
|
|
9275
|
-
const [containerWidth, setContainerWidth] = useState(
|
|
10275
|
+
const [containerWidth, setContainerWidth] = useState(void 0);
|
|
9276
10276
|
useEffect(() => {
|
|
9277
10277
|
const el = containerRef.current;
|
|
9278
10278
|
if (!el) return;
|
|
10279
|
+
const measure = (width) => setContainerWidth(width > 0 ? width : void 0);
|
|
10280
|
+
measure(el.getBoundingClientRect().width);
|
|
10281
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
9279
10282
|
const ro = new ResizeObserver(([entry]) => {
|
|
9280
|
-
if (entry)
|
|
10283
|
+
if (entry) measure(entry.contentRect.width);
|
|
9281
10284
|
});
|
|
9282
10285
|
ro.observe(el);
|
|
9283
10286
|
return () => ro.disconnect();
|
|
9284
10287
|
}, []);
|
|
9285
|
-
const { data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize = 50, paginationEnabled } = tableController;
|
|
9286
|
-
const isLoadingMore = useRef(false);
|
|
9287
|
-
useEffect(() => {
|
|
9288
|
-
if (!dataLoading) isLoadingMore.current = false;
|
|
9289
|
-
}, [dataLoading]);
|
|
10288
|
+
const { data, dataLoading, noMoreToLoad, dataLoadingError, itemCount, setItemCount, pageSize = 50, paginationEnabled, sortBy, setSortBy, filterValues, checkFilterCombination, searchString } = tableController;
|
|
9290
10289
|
const resolvedCollection = collection;
|
|
9291
10290
|
const slotKeys = useCollectionSlotKeys(resolvedCollection, authController, customizationController.propertyConfigs);
|
|
9292
10291
|
const { titleKey: titlePropertyKey, imageKey: imagePropertyKey, subtitleKey, statusKey: statusPropertyKey, dateKey: datePropertyKey } = slotKeys;
|
|
9293
|
-
const
|
|
9294
|
-
|
|
10292
|
+
const columnMode = !!resolvedCollection.listProperties && resolvedCollection.listProperties.length > 0;
|
|
10293
|
+
/**
|
|
10294
|
+
* The properties an `ORDER BY` can be written for, under the same authority
|
|
10295
|
+
* the table headers and the sort menu ask — so a header that offers a sort
|
|
10296
|
+
* here offers it there, and none of the three invents one the driver would
|
|
10297
|
+
* silently drop.
|
|
10298
|
+
*/
|
|
10299
|
+
const sortableKeys = useMemo(() => new Set(getSortablePropertyOptions(resolvedCollection.properties).map((option) => option.key)), [resolvedCollection.properties]);
|
|
10300
|
+
/** The row's identity cell: the leading column, or the title slot. */
|
|
10301
|
+
const titleColumn = useMemo(() => {
|
|
10302
|
+
const key = columnMode ? resolvedCollection.listProperties?.[0] : titlePropertyKey;
|
|
10303
|
+
if (!key) return void 0;
|
|
10304
|
+
const property = resolvedCollection.properties[key];
|
|
10305
|
+
return {
|
|
10306
|
+
key,
|
|
10307
|
+
label: property?.name || (columnMode ? key : "Name"),
|
|
10308
|
+
property,
|
|
10309
|
+
align: "left",
|
|
10310
|
+
width: "flex-1 min-w-0",
|
|
10311
|
+
widthPx: TITLE_COMFORTABLE_WIDTH,
|
|
10312
|
+
sortable: sortableKeys.has(key),
|
|
10313
|
+
priority: Number.MAX_SAFE_INTEGER
|
|
10314
|
+
};
|
|
10315
|
+
}, [
|
|
10316
|
+
columnMode,
|
|
9295
10317
|
resolvedCollection,
|
|
9296
|
-
customizationController.propertyConfigs,
|
|
9297
10318
|
titlePropertyKey,
|
|
9298
|
-
|
|
9299
|
-
statusPropertyKey,
|
|
9300
|
-
datePropertyKey
|
|
10319
|
+
sortableKeys
|
|
9301
10320
|
]);
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
if (!prop) return;
|
|
9315
|
-
cols.push({
|
|
10321
|
+
/**
|
|
10322
|
+
* The trailing columns the collection itself implies: the developer's
|
|
10323
|
+
* `listProperties` when there are any, otherwise the display slots the row
|
|
10324
|
+
* has always shown to the right of the title.
|
|
10325
|
+
*/
|
|
10326
|
+
const declaredColumns = useMemo(() => {
|
|
10327
|
+
if (columnMode) {
|
|
10328
|
+
const keys = resolvedCollection.listProperties.slice(1);
|
|
10329
|
+
return keys.flatMap((key, index) => {
|
|
10330
|
+
const property = resolvedCollection.properties[key];
|
|
10331
|
+
if (!property) return [];
|
|
10332
|
+
return [{
|
|
9316
10333
|
key,
|
|
9317
|
-
label:
|
|
9318
|
-
property
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
return cols;
|
|
9325
|
-
}
|
|
9326
|
-
if (titlePropertyKey) {
|
|
9327
|
-
const prop = resolvedCollection.properties[titlePropertyKey];
|
|
9328
|
-
if (prop) cols.push({
|
|
9329
|
-
key: titlePropertyKey,
|
|
9330
|
-
label: prop.name || "Name",
|
|
9331
|
-
property: prop,
|
|
9332
|
-
isTitle: true,
|
|
9333
|
-
align: "left",
|
|
9334
|
-
width: "flex-1 min-w-[200px]"
|
|
9335
|
-
});
|
|
9336
|
-
}
|
|
9337
|
-
const showDate = datePropertyKey && size !== "xs";
|
|
9338
|
-
const showStatus = statusPropertyKey && size !== "xs" && size !== "s";
|
|
9339
|
-
const subtitleKey = previewKeys.length > 0 ? previewKeys[0] : void 0;
|
|
9340
|
-
const allKeys = resolvedCollection.propertiesOrder || Object.keys(resolvedCollection.properties);
|
|
9341
|
-
const usedKeys = /* @__PURE__ */ new Set([
|
|
9342
|
-
titlePropertyKey,
|
|
9343
|
-
imagePropertyKey,
|
|
9344
|
-
statusPropertyKey,
|
|
9345
|
-
datePropertyKey,
|
|
9346
|
-
subtitleKey,
|
|
9347
|
-
"id"
|
|
9348
|
-
]);
|
|
9349
|
-
allKeys.filter((k) => {
|
|
9350
|
-
if (usedKeys.has(k)) return false;
|
|
9351
|
-
const prop = resolvedCollection.properties[k];
|
|
9352
|
-
if (!prop) return false;
|
|
9353
|
-
if (prop.type === "string" && (prop.storage || prop.admin?.urlPreview === "image")) return false;
|
|
9354
|
-
if (prop.type === "array" && prop.of && !Array.isArray(prop.of)) {
|
|
9355
|
-
const inner = prop.of;
|
|
9356
|
-
if (inner.type === "string" && (inner.storage || inner.admin?.urlPreview === "image")) return false;
|
|
9357
|
-
}
|
|
9358
|
-
return true;
|
|
9359
|
-
}).forEach((key) => {
|
|
9360
|
-
const prop = resolvedCollection.properties[key];
|
|
9361
|
-
cols.push({
|
|
9362
|
-
key,
|
|
9363
|
-
label: prop.name || key,
|
|
9364
|
-
property: prop,
|
|
9365
|
-
align: prop.type === "number" || prop.type === "date" ? "right" : "left",
|
|
9366
|
-
width: getIdealColumnWidth(prop)
|
|
9367
|
-
});
|
|
9368
|
-
});
|
|
9369
|
-
if (showStatus && statusPropertyKey) {
|
|
9370
|
-
const prop = resolvedCollection.properties[statusPropertyKey];
|
|
9371
|
-
if (prop) cols.push({
|
|
9372
|
-
key: statusPropertyKey,
|
|
9373
|
-
label: prop.name || "Status",
|
|
9374
|
-
property: prop,
|
|
9375
|
-
isStatus: true,
|
|
9376
|
-
align: "center",
|
|
9377
|
-
width: "flex-shrink-0 w-32"
|
|
9378
|
-
});
|
|
9379
|
-
}
|
|
9380
|
-
if (showDate && datePropertyKey) {
|
|
9381
|
-
const prop = resolvedCollection.properties[datePropertyKey];
|
|
9382
|
-
if (prop) cols.push({
|
|
9383
|
-
key: datePropertyKey,
|
|
9384
|
-
label: prop.name || "Modified",
|
|
9385
|
-
property: prop,
|
|
9386
|
-
isDate: true,
|
|
9387
|
-
align: "right",
|
|
9388
|
-
width: "flex-shrink-0 w-28"
|
|
10334
|
+
label: property.name || key,
|
|
10335
|
+
property,
|
|
10336
|
+
align: property.type === "number" || property.type === "date" ? "right" : "left",
|
|
10337
|
+
...getIdealColumnWidth(property),
|
|
10338
|
+
sortable: sortableKeys.has(key),
|
|
10339
|
+
priority: PRIORITY_DECLARED + (keys.length - index)
|
|
10340
|
+
}];
|
|
9389
10341
|
});
|
|
9390
10342
|
}
|
|
10343
|
+
const cols = [];
|
|
10344
|
+
const slotColumn = (slot, key, fallbackLabel, layout, priority) => {
|
|
10345
|
+
const declared = hasDeclaredDisplay(resolvedCollection, slot);
|
|
10346
|
+
if (!key && !declared) return void 0;
|
|
10347
|
+
const property = key ? resolvedCollection.properties[key] : void 0;
|
|
10348
|
+
return {
|
|
10349
|
+
key: key ?? `display:${slot}`,
|
|
10350
|
+
label: property?.name || fallbackLabel,
|
|
10351
|
+
slot,
|
|
10352
|
+
property,
|
|
10353
|
+
...layout,
|
|
10354
|
+
sortable: !!key && sortableKeys.has(key),
|
|
10355
|
+
priority
|
|
10356
|
+
};
|
|
10357
|
+
};
|
|
10358
|
+
const tags = slotColumn("tags", slotKeys.tagsKey, "Tags", {
|
|
10359
|
+
align: "left",
|
|
10360
|
+
width: "flex-shrink-0 w-40",
|
|
10361
|
+
widthPx: 160
|
|
10362
|
+
}, PRIORITY_TAGS);
|
|
10363
|
+
if (tags) cols.push(tags);
|
|
10364
|
+
const status = slotColumn("status", statusPropertyKey, "Status", {
|
|
10365
|
+
align: "left",
|
|
10366
|
+
width: "flex-shrink-0 w-32",
|
|
10367
|
+
widthPx: 128
|
|
10368
|
+
}, PRIORITY_STATUS);
|
|
10369
|
+
if (status) cols.push(status);
|
|
10370
|
+
const date = slotColumn("date", datePropertyKey, "Modified", {
|
|
10371
|
+
align: "right",
|
|
10372
|
+
width: "flex-shrink-0 w-20",
|
|
10373
|
+
widthPx: 80
|
|
10374
|
+
}, PRIORITY_DATE);
|
|
10375
|
+
if (date) cols.push(date);
|
|
9391
10376
|
return cols;
|
|
9392
10377
|
}, [
|
|
10378
|
+
columnMode,
|
|
9393
10379
|
resolvedCollection,
|
|
9394
|
-
|
|
10380
|
+
slotKeys.tagsKey,
|
|
9395
10381
|
statusPropertyKey,
|
|
9396
10382
|
datePropertyKey,
|
|
10383
|
+
sortableKeys
|
|
10384
|
+
]);
|
|
10385
|
+
/** What the row already shows without a column of its own. */
|
|
10386
|
+
const shownKeys = useMemo(() => /* @__PURE__ */ new Set([
|
|
10387
|
+
titleColumn?.key,
|
|
9397
10388
|
imagePropertyKey,
|
|
9398
|
-
|
|
9399
|
-
|
|
10389
|
+
subtitleKey,
|
|
10390
|
+
...declaredColumns.map((col) => col.key),
|
|
10391
|
+
...columnMode ? [] : slotKeys.relationKeys
|
|
10392
|
+
]), [
|
|
10393
|
+
titleColumn,
|
|
10394
|
+
imagePropertyKey,
|
|
10395
|
+
subtitleKey,
|
|
10396
|
+
declaredColumns,
|
|
10397
|
+
columnMode,
|
|
10398
|
+
slotKeys.relationKeys
|
|
10399
|
+
]);
|
|
10400
|
+
/** The property the sort names, when no cell already shows it. */
|
|
10401
|
+
const sortedKey = useMemo(() => {
|
|
10402
|
+
const key = sortBy?.[0];
|
|
10403
|
+
return key && !shownKeys.has(key) ? key : void 0;
|
|
10404
|
+
}, [sortBy, shownKeys]);
|
|
10405
|
+
/** The properties the filters name, minus the ones a cell already shows. */
|
|
10406
|
+
const filteredKeys = useMemo(() => Object.keys(filterValues ?? {}).filter((key) => !shownKeys.has(key)), [filterValues, shownKeys]);
|
|
10407
|
+
/**
|
|
10408
|
+
* The last property the sort named, and the last set the filters named.
|
|
10409
|
+
*
|
|
10410
|
+
* A column earns its place when a sort or a filter names it, and keeps it
|
|
10411
|
+
* once that sort or filter is cleared: taking it back would move every
|
|
10412
|
+
* column right of it at the moment the user is comparing values across rows,
|
|
10413
|
+
* and clearing a sort says nothing about wanting to stop seeing the property
|
|
10414
|
+
* it sorted on.
|
|
10415
|
+
*
|
|
10416
|
+
* What it does *not* do is accumulate. There is only ever one sort, so
|
|
10417
|
+
* ordering by four properties in turn is changing one's mind four times, not
|
|
10418
|
+
* asking for four columns — and asking for four is how the row lost its
|
|
10419
|
+
* status and its date to a wall of numbers. A new sort therefore replaces
|
|
10420
|
+
* the property the last one left behind, and a new set of filters replaces
|
|
10421
|
+
* the set before it. At most one sort column and one set of filter columns
|
|
10422
|
+
* outlive their request.
|
|
10423
|
+
*
|
|
10424
|
+
* The list remounts per collection (it is keyed by path), so this is the
|
|
10425
|
+
* memory of one visit to one collection, not a preference.
|
|
10426
|
+
*/
|
|
10427
|
+
const [lastSortedKey, setLastSortedKey] = useState(void 0);
|
|
10428
|
+
const [lastFilteredKeys, setLastFilteredKeys] = useState([]);
|
|
10429
|
+
useEffect(() => {
|
|
10430
|
+
if (sortedKey) setLastSortedKey(sortedKey);
|
|
10431
|
+
}, [sortedKey]);
|
|
10432
|
+
useEffect(() => {
|
|
10433
|
+
if (filteredKeys.length === 0) return;
|
|
10434
|
+
setLastFilteredKeys((previous) => previous.length === filteredKeys.length && previous.every((key, i) => key === filteredKeys[i]) ? previous : filteredKeys);
|
|
10435
|
+
}, [filteredKeys]);
|
|
10436
|
+
/**
|
|
10437
|
+
* Columns for what the user asked to sort or filter by and cannot see.
|
|
10438
|
+
*
|
|
10439
|
+
* Ordering a list by a property no cell shows produces a shuffle with no
|
|
10440
|
+
* explanation in it — the rows move and nothing on screen says why. So the
|
|
10441
|
+
* property joins the row as its own column, last: the filters' first, in
|
|
10442
|
+
* their own order, then the sort's.
|
|
10443
|
+
*/
|
|
10444
|
+
const requestedColumns = useMemo(() => {
|
|
10445
|
+
const active = /* @__PURE__ */ new Set([...filteredKeys, ...sortedKey ? [sortedKey] : []]);
|
|
10446
|
+
const keys = [];
|
|
10447
|
+
const request = (key) => {
|
|
10448
|
+
if (!key || keys.includes(key) || shownKeys.has(key)) return;
|
|
10449
|
+
keys.push(key);
|
|
10450
|
+
};
|
|
10451
|
+
(filteredKeys.length > 0 ? filteredKeys : lastFilteredKeys).forEach(request);
|
|
10452
|
+
request(sortedKey ?? lastSortedKey);
|
|
10453
|
+
return keys.flatMap((key, index) => {
|
|
10454
|
+
const property = getResolvedPropertyInPath(resolvedCollection.properties, key);
|
|
10455
|
+
if (!property) return [];
|
|
10456
|
+
return [{
|
|
10457
|
+
key,
|
|
10458
|
+
label: property.name || key,
|
|
10459
|
+
property,
|
|
10460
|
+
align: property.type === "number" || property.type === "date" ? "right" : "left",
|
|
10461
|
+
...getIdealColumnWidth(property),
|
|
10462
|
+
sortable: sortableKeys.has(key),
|
|
10463
|
+
priority: (active.has(key) ? PRIORITY_ACTIVE_REQUEST : PRIORITY_STALE_REQUEST) - index
|
|
10464
|
+
}];
|
|
10465
|
+
});
|
|
10466
|
+
}, [
|
|
10467
|
+
filteredKeys,
|
|
10468
|
+
sortedKey,
|
|
10469
|
+
lastFilteredKeys,
|
|
10470
|
+
lastSortedKey,
|
|
10471
|
+
shownKeys,
|
|
10472
|
+
resolvedCollection,
|
|
10473
|
+
sortableKeys
|
|
10474
|
+
]);
|
|
10475
|
+
const getListViewActions = useCallback((entity) => {
|
|
10476
|
+
if (!getActionsForEntity) return EMPTY_LIST_VIEW_ACTIONS;
|
|
10477
|
+
return getActionsForEntity({
|
|
10478
|
+
entity,
|
|
10479
|
+
customEntityActions: (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean)
|
|
10480
|
+
}).filter((a) => a.showActionsInListView);
|
|
10481
|
+
}, [
|
|
10482
|
+
getActionsForEntity,
|
|
10483
|
+
collection.entityActions,
|
|
10484
|
+
customizationController.entityActions
|
|
9400
10485
|
]);
|
|
9401
10486
|
const showImage = size !== "xs";
|
|
10487
|
+
/**
|
|
10488
|
+
* Does the checkbox ride on the thumbnail rather than sit beside it?
|
|
10489
|
+
*
|
|
10490
|
+
* Only in the split's compact list — the one layout where the list is a
|
|
10491
|
+
* column beside a record rather than the page itself, and where the 48px of
|
|
10492
|
+
* checkbox cell is 48px the title does not get. Everywhere else the list has
|
|
10493
|
+
* the room, and a checkbox that is always there is easier to find than one
|
|
10494
|
+
* that appears under the pointer.
|
|
10495
|
+
*
|
|
10496
|
+
* Needs a thumbnail to ride on: at `xs` there is none, so the checkbox keeps
|
|
10497
|
+
* its cell.
|
|
10498
|
+
*/
|
|
10499
|
+
const combineSelection = openEntityMode === "split" && selectedEntityId !== void 0 && showImage;
|
|
10500
|
+
/**
|
|
10501
|
+
* How much room the action buttons take, reserved identically in the header
|
|
10502
|
+
* and in every row so the columns line up above one another whether or not a
|
|
10503
|
+
* given record has actions.
|
|
10504
|
+
*/
|
|
10505
|
+
const actionsWidth = useMemo(() => {
|
|
10506
|
+
if (!getActionsForEntity) return 0;
|
|
10507
|
+
let most = 0;
|
|
10508
|
+
for (const entity of data.slice(0, ACTIONS_SAMPLE_SIZE)) most = Math.max(most, getListViewActions(entity).length);
|
|
10509
|
+
return most === 0 ? 0 : most * ACTION_BUTTON_WIDTH + (most - 1) * ACTION_BUTTON_GAP;
|
|
10510
|
+
}, [
|
|
10511
|
+
data,
|
|
10512
|
+
getActionsForEntity,
|
|
10513
|
+
getListViewActions
|
|
10514
|
+
]);
|
|
9402
10515
|
const visibleColumns = useMemo(() => {
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
const
|
|
9407
|
-
const
|
|
9408
|
-
const
|
|
9409
|
-
let
|
|
9410
|
-
if (
|
|
9411
|
-
|
|
10516
|
+
const columns = [...declaredColumns, ...requestedColumns];
|
|
10517
|
+
if (columns.length === 0) return columns;
|
|
10518
|
+
if (containerWidth === void 0) return [];
|
|
10519
|
+
const chrome = ROW_PADDING_WIDTH + (selectionEnabled && !combineSelection ? 48 : 0) + (showImage ? 56 : 0) + (actionsWidth > 0 ? actionsWidth + COLUMN_GAP : 0);
|
|
10520
|
+
const available = containerWidth - chrome - TITLE_COMFORTABLE_WIDTH;
|
|
10521
|
+
const cost = (col) => col.widthPx + COLUMN_GAP;
|
|
10522
|
+
let total = columns.reduce((acc, col) => acc + cost(col), 0);
|
|
10523
|
+
if (total <= available) return columns;
|
|
10524
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
10525
|
+
for (const col of [...columns].sort((a, b) => a.priority - b.priority)) {
|
|
10526
|
+
if (total <= available) break;
|
|
10527
|
+
dropped.add(col.key);
|
|
10528
|
+
total -= cost(col);
|
|
10529
|
+
}
|
|
10530
|
+
return columns.filter((col) => !dropped.has(col.key));
|
|
9412
10531
|
}, [
|
|
9413
|
-
|
|
10532
|
+
declaredColumns,
|
|
10533
|
+
requestedColumns,
|
|
9414
10534
|
containerWidth,
|
|
9415
10535
|
selectionEnabled,
|
|
9416
|
-
|
|
10536
|
+
combineSelection,
|
|
10537
|
+
showImage,
|
|
10538
|
+
actionsWidth
|
|
9417
10539
|
]);
|
|
9418
10540
|
const handleEntityClick = useCallback((entity) => {
|
|
9419
10541
|
analyticsController.onAnalyticsEvent?.("entity_click", {
|
|
@@ -9431,82 +10553,126 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
|
|
|
9431
10553
|
useCallback((entity) => {
|
|
9432
10554
|
return highlightedEntities?.some((e) => e.id === entity.id && e.path === entity.path) ?? false;
|
|
9433
10555
|
}, [highlightedEntities]);
|
|
9434
|
-
const getListViewActions = useCallback((entity) => {
|
|
9435
|
-
if (!getActionsForEntity) return EMPTY_LIST_VIEW_ACTIONS;
|
|
9436
|
-
return getActionsForEntity({
|
|
9437
|
-
entity,
|
|
9438
|
-
customEntityActions: (collection.entityActions ?? []).map((action) => resolveEntityAction(action, customizationController.entityActions)).filter(Boolean)
|
|
9439
|
-
}).filter((a) => a.showActionsInListView);
|
|
9440
|
-
}, [
|
|
9441
|
-
getActionsForEntity,
|
|
9442
|
-
collection.entityActions,
|
|
9443
|
-
customizationController.entityActions
|
|
9444
|
-
]);
|
|
9445
10556
|
const rowClasses = getRowClasses(size);
|
|
9446
10557
|
const selectedIds = useMemo(() => new Set(selectionController?.selectedEntities.map((e) => e.id)), [selectionController?.selectedEntities]);
|
|
9447
10558
|
const highlightedIds = useMemo(() => new Set(highlightedEntities?.map((e) => e.id)), [highlightedEntities]);
|
|
9448
10559
|
const handleRowSelectionChange = useCallback((entity, selected) => {
|
|
9449
10560
|
handleSelectionChange(entity, selected);
|
|
9450
10561
|
}, [handleSelectionChange]);
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
10562
|
+
/**
|
|
10563
|
+
* Order by a column, on the same cycle a table header runs: unordered →
|
|
10564
|
+
* ascending → descending → unordered. Clicking a header here and clicking it
|
|
10565
|
+
* in the table view have to mean the same thing, because they are the same
|
|
10566
|
+
* collection under the same controller.
|
|
10567
|
+
*/
|
|
10568
|
+
const onColumnSort = useCallback((key) => {
|
|
10569
|
+
if (!setSortBy) return;
|
|
10570
|
+
const active = sortBy?.[0] === key ? sortBy[1] : void 0;
|
|
10571
|
+
setSortBy(active === "asc" ? [key, "desc"] : active === "desc" ? void 0 : [key, "asc"]);
|
|
10572
|
+
setItemCount?.(pageSize);
|
|
10573
|
+
}, [
|
|
10574
|
+
setSortBy,
|
|
10575
|
+
sortBy,
|
|
9457
10576
|
setItemCount,
|
|
9458
|
-
pageSize
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
}, [
|
|
9494
|
-
resolvedCollection,
|
|
10577
|
+
pageSize
|
|
10578
|
+
]);
|
|
10579
|
+
/**
|
|
10580
|
+
* Whether a sort can be offered at all next to the filter already applied.
|
|
10581
|
+
* A driver may refuse the combination; the table clears the filter to make
|
|
10582
|
+
* room, which is a heavier answer than a header click asks for, so the
|
|
10583
|
+
* header simply stops offering it — the same choice the sort menu makes.
|
|
10584
|
+
*/
|
|
10585
|
+
const sortIsAvailable = useCallback((key) => {
|
|
10586
|
+
if (!setSortBy) return false;
|
|
10587
|
+
if (!checkFilterCombination) return true;
|
|
10588
|
+
const active = sortBy?.[0] === key ? sortBy[1] : void 0;
|
|
10589
|
+
if (active === "desc") return true;
|
|
10590
|
+
return checkFilterCombination(filterValues ?? {}, [key, active === "asc" ? "desc" : "asc"]);
|
|
10591
|
+
}, [
|
|
10592
|
+
setSortBy,
|
|
10593
|
+
checkFilterCombination,
|
|
10594
|
+
filterValues,
|
|
10595
|
+
sortBy
|
|
10596
|
+
]);
|
|
10597
|
+
return /* @__PURE__ */ jsx("div", {
|
|
10598
|
+
ref: containerRef,
|
|
10599
|
+
className: "w-full",
|
|
10600
|
+
children: /* @__PURE__ */ jsx(ListView, {
|
|
10601
|
+
data,
|
|
10602
|
+
dataLoading,
|
|
10603
|
+
noMoreToLoad,
|
|
10604
|
+
dataLoadingError,
|
|
10605
|
+
itemCount,
|
|
10606
|
+
setItemCount,
|
|
10607
|
+
pageSize,
|
|
10608
|
+
paginationEnabled,
|
|
10609
|
+
onItemClick: handleEntityClick,
|
|
10610
|
+
selectedIds,
|
|
10611
|
+
highlightedIds,
|
|
9495
10612
|
selectionEnabled,
|
|
9496
|
-
|
|
9497
|
-
slotKeys,
|
|
9498
|
-
rowClasses,
|
|
9499
|
-
showImage,
|
|
10613
|
+
emptyComponent,
|
|
9500
10614
|
size,
|
|
9501
10615
|
selectedEntityId,
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
10616
|
+
header: (titleColumn || visibleColumns.length > 0) && /* @__PURE__ */ jsx(ListHeader, {
|
|
10617
|
+
titleColumn,
|
|
10618
|
+
columns: visibleColumns,
|
|
10619
|
+
selectionEnabled: selectionEnabled && !combineSelection,
|
|
10620
|
+
showImage,
|
|
10621
|
+
actionsWidth,
|
|
10622
|
+
sortBy,
|
|
10623
|
+
onColumnSort,
|
|
10624
|
+
sortIsAvailable
|
|
10625
|
+
}),
|
|
10626
|
+
renderRow: useCallback(({ item: entity, style, className, selected, highlighted, isLast, onClick, onSelectionChange }) => {
|
|
10627
|
+
return /* @__PURE__ */ jsx("div", {
|
|
10628
|
+
style,
|
|
10629
|
+
className,
|
|
10630
|
+
children: /* @__PURE__ */ jsx(ListRow, {
|
|
10631
|
+
entity,
|
|
10632
|
+
collection: resolvedCollection,
|
|
10633
|
+
searchString,
|
|
10634
|
+
onClick: handleEntityClick,
|
|
10635
|
+
selected,
|
|
10636
|
+
highlighted,
|
|
10637
|
+
onSelectionChange: handleRowSelectionChange,
|
|
10638
|
+
selectionEnabled,
|
|
10639
|
+
combineSelection,
|
|
10640
|
+
columns: visibleColumns,
|
|
10641
|
+
slotKeys,
|
|
10642
|
+
rowClasses,
|
|
10643
|
+
showImage,
|
|
10644
|
+
size,
|
|
10645
|
+
isLast,
|
|
10646
|
+
isActive: selectedEntityId !== void 0 && entity.id === selectedEntityId,
|
|
10647
|
+
listViewActions: getListViewActions(entity),
|
|
10648
|
+
actionsWidth,
|
|
10649
|
+
context,
|
|
10650
|
+
path,
|
|
10651
|
+
selectionController,
|
|
10652
|
+
openEntityMode
|
|
10653
|
+
})
|
|
10654
|
+
}, entity.id);
|
|
10655
|
+
}, [
|
|
10656
|
+
resolvedCollection,
|
|
10657
|
+
selectionEnabled,
|
|
10658
|
+
combineSelection,
|
|
10659
|
+
visibleColumns,
|
|
10660
|
+
slotKeys,
|
|
10661
|
+
rowClasses,
|
|
10662
|
+
showImage,
|
|
10663
|
+
size,
|
|
10664
|
+
selectedEntityId,
|
|
10665
|
+
getListViewActions,
|
|
10666
|
+
actionsWidth,
|
|
10667
|
+
context,
|
|
10668
|
+
path,
|
|
10669
|
+
selectionController,
|
|
10670
|
+
openEntityMode,
|
|
10671
|
+
handleRowSelectionChange,
|
|
10672
|
+
handleEntityClick,
|
|
10673
|
+
searchString
|
|
10674
|
+
])
|
|
10675
|
+
})
|
|
9510
10676
|
});
|
|
9511
10677
|
}
|
|
9512
10678
|
/**
|
|
@@ -9517,8 +10683,9 @@ function CollectionListViewBinding({ collection, tableController, onEntityClick,
|
|
|
9517
10683
|
* When collection.listProperties is explicitly set, falls back to
|
|
9518
10684
|
* the column system for developer-defined table layouts.
|
|
9519
10685
|
*/
|
|
9520
|
-
var ListRow = React.memo(function ListRow({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, columns, slotKeys, rowClasses, showImage, size, isLast, isActive = false, listViewActions = [], context, path, selectionController, openEntityMode }) {
|
|
10686
|
+
var ListRow = React.memo(function ListRow({ entity, collection, onClick, selected, highlighted, onSelectionChange, selectionEnabled, combineSelection = false, columns, slotKeys, rowClasses, showImage, size, isLast, isActive = false, listViewActions = [], actionsWidth = 0, context, path, selectionController, openEntityMode, searchString }) {
|
|
9521
10687
|
const slots = useEntitySlots(entity, collection, slotKeys);
|
|
10688
|
+
const { terms, offSlot } = useSearchExplanation(entity, collection.properties, [slotKeys.titleKey, slotKeys.subtitleKey], searchString);
|
|
9522
10689
|
const handleClick = useCallback((e) => {
|
|
9523
10690
|
if ((e.metaKey || e.ctrlKey) && selectionEnabled) {
|
|
9524
10691
|
e.preventDefault();
|
|
@@ -9539,14 +10706,22 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9539
10706
|
const handleCheckboxChange = useCallback((checked) => {
|
|
9540
10707
|
onSelectionChange?.(entity, checked);
|
|
9541
10708
|
}, [entity, onSelectionChange]);
|
|
10709
|
+
/** The thumbnail-as-checkbox cell: the square toggles, and never opens the record. */
|
|
10710
|
+
const handleSelectionCellClick = useCallback((e) => {
|
|
10711
|
+
e.stopPropagation();
|
|
10712
|
+
onSelectionChange?.(entity, !selected);
|
|
10713
|
+
}, [
|
|
10714
|
+
entity,
|
|
10715
|
+
onSelectionChange,
|
|
10716
|
+
selected
|
|
10717
|
+
]);
|
|
9542
10718
|
const useColumnMode = !!collection.listProperties && collection.listProperties.length > 0;
|
|
9543
|
-
const extraColumns = useColumnMode ? columns.filter((c) => !c.isTitle) : [];
|
|
9544
10719
|
return /* @__PURE__ */ jsxs("div", {
|
|
9545
10720
|
className: cls("@container flex items-center gap-4 cursor-pointer group transition-colors duration-200 relative h-full", rowClasses, isActive ? "bg-surface-accent-100 dark:bg-surface-accent-950 hover:bg-surface-accent-200 dark:hover:bg-surface-accent-950" : selected ? "bg-surface-accent-50 dark:bg-surface-accent-900 hover:bg-surface-accent-100 dark:hover:bg-surface-accent-950" : highlighted ? "bg-surface-accent-50 dark:bg-surface-accent-900 hover:bg-surface-50 dark:hover:bg-surface-800/40" : "bg-white dark:bg-surface-900 hover:bg-surface-50 dark:hover:bg-surface-800/40"),
|
|
9546
10721
|
onClick: handleClick,
|
|
9547
10722
|
children: [
|
|
9548
10723
|
selected && !isActive && /* @__PURE__ */ jsx("div", { className: "absolute left-0 top-0 bottom-0 w-0.5 bg-primary-500 rounded-r-full" }),
|
|
9549
|
-
selectionEnabled && /* @__PURE__ */ jsx("div", {
|
|
10724
|
+
selectionEnabled && !combineSelection && /* @__PURE__ */ jsx("div", {
|
|
9550
10725
|
className: "flex-shrink-0 w-8",
|
|
9551
10726
|
onClick: handleCheckboxClick,
|
|
9552
10727
|
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
@@ -9555,9 +10730,9 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9555
10730
|
size: "smallest"
|
|
9556
10731
|
})
|
|
9557
10732
|
}),
|
|
9558
|
-
showImage && /* @__PURE__ */
|
|
9559
|
-
className: "flex-shrink-0",
|
|
9560
|
-
children: slots.image ? /* @__PURE__ */ jsx("div", {
|
|
10733
|
+
showImage && /* @__PURE__ */ jsxs("div", {
|
|
10734
|
+
className: "flex-shrink-0 relative w-10 h-10",
|
|
10735
|
+
children: [slots.image ? /* @__PURE__ */ jsx("div", {
|
|
9561
10736
|
className: cls("w-10 h-10 rounded-lg border relative overflow-hidden bg-surface-100 dark:bg-surface-900", defaultBorderMixin),
|
|
9562
10737
|
children: /* @__PURE__ */ jsx(SlotValue, {
|
|
9563
10738
|
slot: slots.image,
|
|
@@ -9571,10 +10746,21 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9571
10746
|
className: "text-surface-500 dark:text-surface-400",
|
|
9572
10747
|
size: "small"
|
|
9573
10748
|
})
|
|
9574
|
-
})
|
|
10749
|
+
}), selectionEnabled && combineSelection && /* @__PURE__ */ jsx("div", {
|
|
10750
|
+
className: cls("absolute inset-0 flex items-center justify-center rounded-lg transition-opacity duration-150", "bg-surface-950/55", selected ? "opacity-100" : "opacity-0 group-hover:opacity-100 focus-within:opacity-100"),
|
|
10751
|
+
onClick: handleSelectionCellClick,
|
|
10752
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
10753
|
+
className: "pointer-events-none",
|
|
10754
|
+
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
10755
|
+
checked: selected ?? false,
|
|
10756
|
+
onCheckedChange: handleCheckboxChange,
|
|
10757
|
+
size: "smallest"
|
|
10758
|
+
})
|
|
10759
|
+
})
|
|
10760
|
+
})]
|
|
9575
10761
|
}),
|
|
9576
10762
|
/* @__PURE__ */ jsxs("div", {
|
|
9577
|
-
className:
|
|
10763
|
+
className: "flex-1 min-w-0 overflow-hidden",
|
|
9578
10764
|
children: [
|
|
9579
10765
|
/* @__PURE__ */ jsx("div", {
|
|
9580
10766
|
className: "truncate",
|
|
@@ -9582,7 +10768,10 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9582
10768
|
component: "div",
|
|
9583
10769
|
variant: "body2",
|
|
9584
10770
|
className: "font-semibold text-surface-900 dark:text-surface-50 truncate transition-colors group-hover:text-primary-600 dark:group-hover:text-primary-400",
|
|
9585
|
-
children: /* @__PURE__ */ jsx(
|
|
10771
|
+
children: terms.length > 0 && typeof slots.title?.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
|
|
10772
|
+
text: slots.title.value,
|
|
10773
|
+
terms
|
|
10774
|
+
}) : /* @__PURE__ */ jsx(SlotValue, {
|
|
9586
10775
|
slot: slots.title,
|
|
9587
10776
|
size: "small"
|
|
9588
10777
|
})
|
|
@@ -9593,13 +10782,19 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9593
10782
|
children: entity.id
|
|
9594
10783
|
})
|
|
9595
10784
|
}),
|
|
9596
|
-
|
|
10785
|
+
offSlot ? /* @__PURE__ */ jsx(MatchExplanation, {
|
|
10786
|
+
match: offSlot,
|
|
10787
|
+
label: fieldLabel(collection.properties, offSlot.field)
|
|
10788
|
+
}) : slots.subtitle && /* @__PURE__ */ jsx("div", {
|
|
9597
10789
|
className: "truncate mt-0.5",
|
|
9598
10790
|
children: /* @__PURE__ */ jsx(Typography, {
|
|
9599
10791
|
variant: "caption",
|
|
9600
10792
|
component: "div",
|
|
9601
10793
|
className: "text-surface-500 dark:text-surface-400 truncate",
|
|
9602
|
-
children: /* @__PURE__ */ jsx(
|
|
10794
|
+
children: terms.length > 0 && typeof slots.subtitle.value === "string" ? /* @__PURE__ */ jsx(Highlighted, {
|
|
10795
|
+
text: slots.subtitle.value,
|
|
10796
|
+
terms
|
|
10797
|
+
}) : /* @__PURE__ */ jsx(SlotValue, {
|
|
9603
10798
|
slot: slots.subtitle,
|
|
9604
10799
|
size: "small"
|
|
9605
10800
|
})
|
|
@@ -9619,72 +10814,20 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9619
10814
|
})
|
|
9620
10815
|
]
|
|
9621
10816
|
}),
|
|
9622
|
-
|
|
9623
|
-
const value = getValueInPath(entity.values, col.key);
|
|
9624
|
-
if (col.isStatus || col.property.type === "string" && "enum" in col.property) return /* @__PURE__ */ jsx("div", {
|
|
9625
|
-
className: cls(col.width, "flex", col.align === "center" ? "justify-center" : col.align === "right" ? "justify-end" : "justify-start"),
|
|
9626
|
-
children: value ? /* @__PURE__ */ jsx(PropertyPreview, {
|
|
9627
|
-
propertyKey: col.key,
|
|
9628
|
-
value,
|
|
9629
|
-
property: col.property,
|
|
9630
|
-
size: "small"
|
|
9631
|
-
}) : /* @__PURE__ */ jsx("span", {
|
|
9632
|
-
className: "text-surface-400",
|
|
9633
|
-
children: "—"
|
|
9634
|
-
})
|
|
9635
|
-
}, col.key);
|
|
9636
|
-
if (col.isDate || col.property.type === "date") return /* @__PURE__ */ jsx("div", {
|
|
9637
|
-
className: cls(col.width, col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : "text-left"),
|
|
9638
|
-
children: /* @__PURE__ */ jsx(Typography, {
|
|
9639
|
-
variant: "caption",
|
|
9640
|
-
className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
|
|
9641
|
-
children: formatDateValue(value) ?? "—"
|
|
9642
|
-
})
|
|
9643
|
-
}, col.key);
|
|
9644
|
-
if (isComplexPropertyType(col.property)) {
|
|
9645
|
-
const summary = compactValueSummary(value, col.property);
|
|
9646
|
-
return /* @__PURE__ */ jsx("div", {
|
|
9647
|
-
className: cls(col.width, "truncate", col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : "text-left"),
|
|
9648
|
-
children: /* @__PURE__ */ jsx(Typography, {
|
|
9649
|
-
variant: "caption",
|
|
9650
|
-
className: "text-surface-500 dark:text-surface-400 truncate",
|
|
9651
|
-
children: summary ?? "—"
|
|
9652
|
-
})
|
|
9653
|
-
}, col.key);
|
|
9654
|
-
}
|
|
9655
|
-
return /* @__PURE__ */ jsx("div", {
|
|
9656
|
-
className: cls(col.width, "truncate", col.align === "center" ? "text-center" : col.align === "right" ? "text-right" : "text-left"),
|
|
9657
|
-
children: /* @__PURE__ */ jsx(Typography, {
|
|
9658
|
-
component: "div",
|
|
9659
|
-
variant: "body2",
|
|
9660
|
-
className: "text-surface-600 dark:text-surface-300 truncate",
|
|
9661
|
-
children: value !== void 0 ? /* @__PURE__ */ jsx(PropertyPreview, {
|
|
9662
|
-
propertyKey: col.key,
|
|
9663
|
-
value,
|
|
9664
|
-
property: col.property,
|
|
9665
|
-
size: "small"
|
|
9666
|
-
}) : "—"
|
|
9667
|
-
})
|
|
9668
|
-
}, col.key);
|
|
9669
|
-
}) }) : /* @__PURE__ */ jsxs("div", {
|
|
10817
|
+
columns.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
9670
10818
|
className: "flex items-center gap-4 flex-shrink-0 ml-auto",
|
|
9671
|
-
children:
|
|
9672
|
-
className: "flex-
|
|
9673
|
-
children: /* @__PURE__ */ jsx(
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
}), slots.date && /* @__PURE__ */ jsx("div", {
|
|
9678
|
-
className: "flex-shrink-0 text-right w-[80px] @max-[500px]:hidden",
|
|
9679
|
-
children: /* @__PURE__ */ jsx(Typography, {
|
|
9680
|
-
variant: "caption",
|
|
9681
|
-
className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
|
|
9682
|
-
children: slots.date.formatted ?? "—"
|
|
10819
|
+
children: columns.map((col) => /* @__PURE__ */ jsx("div", {
|
|
10820
|
+
className: cls(col.width, "flex items-center overflow-hidden", col.align === "center" ? "justify-center" : col.align === "right" ? "justify-end" : "justify-start"),
|
|
10821
|
+
children: /* @__PURE__ */ jsx(ListCell, {
|
|
10822
|
+
column: col,
|
|
10823
|
+
entity,
|
|
10824
|
+
slots
|
|
9683
10825
|
})
|
|
9684
|
-
})
|
|
10826
|
+
}, col.key))
|
|
9685
10827
|
}),
|
|
9686
|
-
|
|
9687
|
-
className: "flex items-center gap-0.5 flex-shrink-0 ml-auto",
|
|
10828
|
+
actionsWidth > 0 && /* @__PURE__ */ jsx("div", {
|
|
10829
|
+
className: "flex items-center justify-end gap-0.5 flex-shrink-0 ml-auto",
|
|
10830
|
+
style: { minWidth: actionsWidth },
|
|
9688
10831
|
onClick: (e) => e.stopPropagation(),
|
|
9689
10832
|
children: listViewActions.map((action, index) => /* @__PURE__ */ jsx(Tooltip, {
|
|
9690
10833
|
title: action.name,
|
|
@@ -9711,6 +10854,143 @@ var ListRow = React.memo(function ListRow({ entity, collection, onClick, selecte
|
|
|
9711
10854
|
]
|
|
9712
10855
|
});
|
|
9713
10856
|
});
|
|
10857
|
+
/**
|
|
10858
|
+
* One cell of one row: whatever the column says fills it.
|
|
10859
|
+
*
|
|
10860
|
+
* A slot column renders the slot — a status keeps its coloured chip and a date
|
|
10861
|
+
* its relative wording wherever the value came from — and a property column
|
|
10862
|
+
* reads the record. Both go through here so the header above them is labelling
|
|
10863
|
+
* one thing, not two that happen to line up.
|
|
10864
|
+
*/
|
|
10865
|
+
function ListCell({ column, entity, slots }) {
|
|
10866
|
+
if (column.slot === "tags") {
|
|
10867
|
+
if (!slots.tags) return null;
|
|
10868
|
+
return /* @__PURE__ */ jsx("div", {
|
|
10869
|
+
className: "flex items-center gap-1 overflow-hidden",
|
|
10870
|
+
children: /* @__PURE__ */ jsx(TagChips, {
|
|
10871
|
+
slot: slots.tags,
|
|
10872
|
+
max: 3
|
|
10873
|
+
})
|
|
10874
|
+
});
|
|
10875
|
+
}
|
|
10876
|
+
if (column.slot === "status") {
|
|
10877
|
+
if (!slots.status) return /* @__PURE__ */ jsx(EmptyCell, {});
|
|
10878
|
+
return /* @__PURE__ */ jsx(SlotValue, {
|
|
10879
|
+
slot: slots.status,
|
|
10880
|
+
size: "small"
|
|
10881
|
+
});
|
|
10882
|
+
}
|
|
10883
|
+
if (column.slot === "date") return /* @__PURE__ */ jsx(Typography, {
|
|
10884
|
+
variant: "caption",
|
|
10885
|
+
className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
|
|
10886
|
+
children: slots.date?.formatted ?? "—"
|
|
10887
|
+
});
|
|
10888
|
+
const property = column.property;
|
|
10889
|
+
if (!property) return null;
|
|
10890
|
+
const value = getValueInPath(entity.values, column.key);
|
|
10891
|
+
if (property.type === "date") return /* @__PURE__ */ jsx(Typography, {
|
|
10892
|
+
variant: "caption",
|
|
10893
|
+
className: "whitespace-nowrap text-surface-400 dark:text-surface-500 font-medium",
|
|
10894
|
+
children: formatDateValue(value) ?? "—"
|
|
10895
|
+
});
|
|
10896
|
+
if (isComplexPropertyType(property)) return /* @__PURE__ */ jsx(Typography, {
|
|
10897
|
+
variant: "caption",
|
|
10898
|
+
className: "text-surface-500 dark:text-surface-400 truncate",
|
|
10899
|
+
children: compactValueSummary(value, property) ?? "—"
|
|
10900
|
+
});
|
|
10901
|
+
if (value === void 0 || value === null) return /* @__PURE__ */ jsx(EmptyCell, {});
|
|
10902
|
+
return /* @__PURE__ */ jsx(Typography, {
|
|
10903
|
+
component: "div",
|
|
10904
|
+
variant: "body2",
|
|
10905
|
+
className: "text-surface-600 dark:text-surface-300 truncate",
|
|
10906
|
+
children: /* @__PURE__ */ jsx(PropertyPreview, {
|
|
10907
|
+
propertyKey: column.key,
|
|
10908
|
+
value,
|
|
10909
|
+
property,
|
|
10910
|
+
size: "small"
|
|
10911
|
+
})
|
|
10912
|
+
});
|
|
10913
|
+
}
|
|
10914
|
+
/** What a column shows for a record that has nothing in it. */
|
|
10915
|
+
function EmptyCell() {
|
|
10916
|
+
return /* @__PURE__ */ jsx("span", {
|
|
10917
|
+
className: "text-surface-400 dark:text-surface-600",
|
|
10918
|
+
children: "—"
|
|
10919
|
+
});
|
|
10920
|
+
}
|
|
10921
|
+
/**
|
|
10922
|
+
* The list's column header.
|
|
10923
|
+
*
|
|
10924
|
+
* Quiet by design: a list is read row by row, and the header is there to name
|
|
10925
|
+
* the columns and to be clicked, not to compete with the rows. It lays out
|
|
10926
|
+
* against the same widths the rows do, and skips the checkbox and the image —
|
|
10927
|
+
* neither is a column, and neither has anything to be called.
|
|
10928
|
+
*/
|
|
10929
|
+
function ListHeader({ titleColumn, columns, selectionEnabled, showImage, actionsWidth, sortBy, onColumnSort, sortIsAvailable }) {
|
|
10930
|
+
const headerCell = (column) => /* @__PURE__ */ jsx(ListHeaderLabel, {
|
|
10931
|
+
column,
|
|
10932
|
+
direction: sortBy?.[0] === column.key ? sortBy[1] : void 0,
|
|
10933
|
+
onSort: column.sortable && sortIsAvailable(column.key) ? () => onColumnSort(column.key) : void 0
|
|
10934
|
+
});
|
|
10935
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
10936
|
+
className: cls("flex items-center gap-4 px-5 py-1.5 select-none border-b bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
|
|
10937
|
+
children: [
|
|
10938
|
+
selectionEnabled && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 w-8" }),
|
|
10939
|
+
showImage && /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 w-10" }),
|
|
10940
|
+
/* @__PURE__ */ jsx("div", {
|
|
10941
|
+
className: "flex-1 min-w-0 flex items-center",
|
|
10942
|
+
children: titleColumn && headerCell(titleColumn)
|
|
10943
|
+
}),
|
|
10944
|
+
columns.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
10945
|
+
className: "flex items-center gap-4 flex-shrink-0 ml-auto",
|
|
10946
|
+
children: columns.map((column) => /* @__PURE__ */ jsx("div", {
|
|
10947
|
+
className: cls(column.width, "flex items-center overflow-hidden", column.align === "center" ? "justify-center" : column.align === "right" ? "justify-end" : "justify-start"),
|
|
10948
|
+
children: headerCell(column)
|
|
10949
|
+
}, column.key))
|
|
10950
|
+
}),
|
|
10951
|
+
actionsWidth > 0 && /* @__PURE__ */ jsx("div", {
|
|
10952
|
+
className: "flex-shrink-0 ml-auto",
|
|
10953
|
+
style: { minWidth: actionsWidth }
|
|
10954
|
+
})
|
|
10955
|
+
]
|
|
10956
|
+
});
|
|
10957
|
+
}
|
|
10958
|
+
/**
|
|
10959
|
+
* One header label, clickable when the collection can be ordered by it.
|
|
10960
|
+
*
|
|
10961
|
+
* The arrow shows only on the column currently ordering the list: three arrows
|
|
10962
|
+
* in a header row are three invitations, and only one of them is ever the
|
|
10963
|
+
* answer to "how is this sorted".
|
|
10964
|
+
*/
|
|
10965
|
+
function ListHeaderLabel({ column, direction, onSort }) {
|
|
10966
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
10967
|
+
/* @__PURE__ */ jsx("span", {
|
|
10968
|
+
className: "truncate",
|
|
10969
|
+
children: column.label
|
|
10970
|
+
}),
|
|
10971
|
+
direction === "asc" && /* @__PURE__ */ jsx(ArrowUpIcon, {
|
|
10972
|
+
size: 12,
|
|
10973
|
+
className: "flex-shrink-0"
|
|
10974
|
+
}),
|
|
10975
|
+
direction === "desc" && /* @__PURE__ */ jsx(ArrowDownIcon, {
|
|
10976
|
+
size: 12,
|
|
10977
|
+
className: "flex-shrink-0"
|
|
10978
|
+
})
|
|
10979
|
+
] });
|
|
10980
|
+
const base = "inline-flex items-center gap-1 max-w-full text-[11px] leading-none uppercase tracking-wider font-medium";
|
|
10981
|
+
const tone = direction ? "text-surface-600 dark:text-surface-300" : "text-surface-400 dark:text-surface-500";
|
|
10982
|
+
if (!onSort) return /* @__PURE__ */ jsx("span", {
|
|
10983
|
+
className: cls(base, tone),
|
|
10984
|
+
children: content
|
|
10985
|
+
});
|
|
10986
|
+
return /* @__PURE__ */ jsx("button", {
|
|
10987
|
+
type: "button",
|
|
10988
|
+
title: `Sort by ${column.label}`,
|
|
10989
|
+
onClick: onSort,
|
|
10990
|
+
className: cls(base, tone, "cursor-pointer hover:text-surface-700 dark:hover:text-surface-200 transition-colors"),
|
|
10991
|
+
children: content
|
|
10992
|
+
});
|
|
10993
|
+
}
|
|
9714
10994
|
//#endregion
|
|
9715
10995
|
//#region src/components/EntityDisplayHeader.tsx
|
|
9716
10996
|
/**
|
|
@@ -10056,11 +11336,11 @@ function EntityViewBinding({ entity, collection, className, asPage = false, open
|
|
|
10056
11336
|
});
|
|
10057
11337
|
return /* @__PURE__ */ jsxs("div", {
|
|
10058
11338
|
ref: containerRef,
|
|
10059
|
-
className: cls("flex-1 flex flex-row w-full min-h-0", className),
|
|
11339
|
+
className: cls("@container/form flex-1 flex flex-row w-full min-h-0", className),
|
|
10060
11340
|
children: [/* @__PURE__ */ jsx("div", {
|
|
10061
11341
|
className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
|
|
10062
11342
|
children: /* @__PURE__ */ jsx("div", {
|
|
10063
|
-
className: cls("@container/col w-full max-w-3xl 2xl:max-w-4xl flex flex-col", openEntityMode === "dialog" ? "pt-5 pb-8 px-6 sm:px-8" : "pt-6 pb-12 px-5 sm:px-8"),
|
|
11343
|
+
className: cls("@container/col w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col", openEntityMode === "dialog" ? "pt-5 pb-8 px-6 sm:px-8" : "pt-6 pb-12 px-5 sm:px-8"),
|
|
10064
11344
|
children: content
|
|
10065
11345
|
})
|
|
10066
11346
|
}), showRail && /* @__PURE__ */ jsx(FormRail, {
|
|
@@ -10083,11 +11363,14 @@ function EntityViewBinding({ entity, collection, className, asPage = false, open
|
|
|
10083
11363
|
* editing. They live here instead, and the left half of a bar that was
|
|
10084
11364
|
* previously empty now does the work.
|
|
10085
11365
|
*/
|
|
10086
|
-
function EntityIdentityBar({ collection, collectionUrl, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, onBack, onInspect, onViewHistory, externalLink, recordActions, pluginActions, trailing, leading }) {
|
|
11366
|
+
function EntityIdentityBar({ collection, collectionUrl, title, entityId, status, dirty, saving, onSave, onDiscard, saveDisabled, hasErrors, onSaveAndClose, saveAndClosePlacement = "button", onClose, onBack, onInspect, onViewHistory, externalLink, recordActions, pluginActions, trailing, leading }) {
|
|
10087
11367
|
const { t } = useTranslation();
|
|
10088
11368
|
const saveLabel = status === "existing" ? t("save") : status === "copy" ? t("create_copy") : t("create");
|
|
10089
11369
|
const closeLabel = status === "existing" ? t("save_and_close") : status === "copy" ? t("create_copy_and_close") : t("create_and_close");
|
|
10090
11370
|
const hasMenu = Boolean(recordActions) || Boolean(onInspect) || Boolean(onViewHistory) || Boolean(externalLink);
|
|
11371
|
+
const saveAndCloseButton = Boolean(onSaveAndClose) && saveAndClosePlacement === "button";
|
|
11372
|
+
const saveAndCloseMenu = Boolean(onSaveAndClose) && saveAndClosePlacement === "menu";
|
|
11373
|
+
const saveTooltip = hasErrors ? t("fix_errors_before_saving") ?? "Fix highlighted errors before saving" : void 0;
|
|
10091
11374
|
return /* @__PURE__ */ jsxs("div", {
|
|
10092
11375
|
className: cls("h-[52px] shrink-0 flex items-center gap-2 pl-1.5 pr-2 border-b", "bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
|
|
10093
11376
|
children: [
|
|
@@ -10134,19 +11417,39 @@ function EntityIdentityBar({ collection, collectionUrl, title, entityId, status,
|
|
|
10134
11417
|
onClick: onDiscard,
|
|
10135
11418
|
children: status === "existing" ? t("discard") : t("clear")
|
|
10136
11419
|
}),
|
|
10137
|
-
onSave && /* @__PURE__ */
|
|
10138
|
-
|
|
10139
|
-
children: /* @__PURE__ */ jsx(
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
11420
|
+
onSave && /* @__PURE__ */ jsxs("div", {
|
|
11421
|
+
className: "flex items-stretch rounded-lg overflow-hidden",
|
|
11422
|
+
children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
11423
|
+
title: saveTooltip,
|
|
11424
|
+
children: /* @__PURE__ */ jsx(LoadingButton, {
|
|
11425
|
+
variant: saveAndCloseButton ? "text" : "filled",
|
|
11426
|
+
color: "primary",
|
|
11427
|
+
size: "small",
|
|
11428
|
+
loading: saving && !saveAndCloseButton,
|
|
11429
|
+
disabled: saveDisabled,
|
|
11430
|
+
onClick: onSave,
|
|
11431
|
+
className: saveAndCloseMenu ? "rounded-none" : void 0,
|
|
11432
|
+
children: saveLabel
|
|
11433
|
+
})
|
|
11434
|
+
}), saveAndCloseMenu && /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(Menu, {
|
|
11435
|
+
align: "end",
|
|
11436
|
+
trigger: /* @__PURE__ */ jsx(Button, {
|
|
11437
|
+
variant: "filled",
|
|
11438
|
+
color: "primary",
|
|
11439
|
+
size: "small",
|
|
11440
|
+
disabled: saveDisabled,
|
|
11441
|
+
"aria-label": closeLabel,
|
|
11442
|
+
style: { borderLeftColor: "color-mix(in oklab, currentColor 15%, transparent)" },
|
|
11443
|
+
className: "rounded-none px-1.5",
|
|
11444
|
+
children: /* @__PURE__ */ jsx(ChevronDownIcon, { size: iconSize.smallest })
|
|
11445
|
+
}),
|
|
11446
|
+
children: /* @__PURE__ */ jsxs(MenuItem, {
|
|
11447
|
+
onClick: onSaveAndClose,
|
|
11448
|
+
children: [/* @__PURE__ */ jsx(CheckIcon, { size: iconSize.smallest }), closeLabel]
|
|
11449
|
+
})
|
|
11450
|
+
}) })]
|
|
10148
11451
|
}),
|
|
10149
|
-
|
|
11452
|
+
saveAndCloseButton && /* @__PURE__ */ jsx(LoadingButton, {
|
|
10150
11453
|
variant: "filled",
|
|
10151
11454
|
color: "primary",
|
|
10152
11455
|
size: "small",
|
|
@@ -10180,7 +11483,19 @@ function EntityIdentityBar({ collection, collectionUrl, title, entityId, status,
|
|
|
10180
11483
|
})
|
|
10181
11484
|
]
|
|
10182
11485
|
}),
|
|
10183
|
-
trailing
|
|
11486
|
+
trailing,
|
|
11487
|
+
onClose && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Separator, {
|
|
11488
|
+
orientation: "vertical",
|
|
11489
|
+
className: "h-5 mx-1 shrink-0"
|
|
11490
|
+
}), /* @__PURE__ */ jsx(Tooltip, {
|
|
11491
|
+
title: `${t("close")} · Esc`,
|
|
11492
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
11493
|
+
size: "small",
|
|
11494
|
+
onClick: onClose,
|
|
11495
|
+
"aria-label": t("close"),
|
|
11496
|
+
children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
|
|
11497
|
+
})
|
|
11498
|
+
})] })
|
|
10184
11499
|
]
|
|
10185
11500
|
});
|
|
10186
11501
|
}
|
|
@@ -10250,13 +11565,17 @@ function SplitViewProvider({ value, children }) {
|
|
|
10250
11565
|
* Closes the list of a split view, leaving the open record on its own.
|
|
10251
11566
|
*
|
|
10252
11567
|
* It leads the record's own app bar, where the breadcrumb starts — this is the
|
|
10253
|
-
* entity pane's old "Open full screen" button, moved to that edge
|
|
10254
|
-
*
|
|
10255
|
-
*
|
|
10256
|
-
*
|
|
10257
|
-
*
|
|
11568
|
+
* entity pane's old "Open full screen" button, moved to that edge: closing the
|
|
11569
|
+
* list and opening the record full screen are the same thing, so there is one
|
|
11570
|
+
* control and one route (`#full`) rather than two. Full screen carries the
|
|
11571
|
+
* mirrored icon to bring the list back.
|
|
11572
|
+
*
|
|
11573
|
+
* A pane glyph rather than the double chevron it wore first. Now that the bar
|
|
11574
|
+
* ends in a ✕ that dismisses the record, an arrow pointing off the left edge of
|
|
11575
|
+
* the record's own bar was the other thing on it that read as "close me" — and
|
|
11576
|
+
* it is the one control here that does not act on the record at all.
|
|
10258
11577
|
*/
|
|
10259
|
-
function SplitListCloseButton() {
|
|
11578
|
+
function SplitListCloseButton({ onBeforeHide } = {}) {
|
|
10260
11579
|
const { t } = useTranslation();
|
|
10261
11580
|
const splitView = useSplitView();
|
|
10262
11581
|
if (!splitView?.detailOpen) return null;
|
|
@@ -10264,9 +11583,12 @@ function SplitListCloseButton() {
|
|
|
10264
11583
|
title: t("hide_list"),
|
|
10265
11584
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
10266
11585
|
size: "small",
|
|
10267
|
-
onClick:
|
|
11586
|
+
onClick: () => {
|
|
11587
|
+
onBeforeHide?.();
|
|
11588
|
+
splitView.openFullScreen();
|
|
11589
|
+
},
|
|
10268
11590
|
"aria-label": t("hide_list"),
|
|
10269
|
-
children: /* @__PURE__ */ jsx(
|
|
11591
|
+
children: /* @__PURE__ */ jsx(PanelLeftCloseIcon, { size: iconSize.smallest })
|
|
10270
11592
|
})
|
|
10271
11593
|
});
|
|
10272
11594
|
}
|
|
@@ -10276,7 +11598,7 @@ function SplitListCloseButton() {
|
|
|
10276
11598
|
* Brings the list back beside a record that was opened full screen.
|
|
10277
11599
|
*
|
|
10278
11600
|
* The mirror of {@link SplitListCloseButton}: the same edge of the same bar,
|
|
10279
|
-
* the same
|
|
11601
|
+
* the same pane glyph opening rather than closing, and the same single route —
|
|
10280
11602
|
* showing the list is dropping the `#full` hash. It replaces the back arrow
|
|
10281
11603
|
* where it applies, because the list it unfolds already holds the record.
|
|
10282
11604
|
*/
|
|
@@ -10288,7 +11610,7 @@ function SplitListShowButton({ onClick }) {
|
|
|
10288
11610
|
size: "small",
|
|
10289
11611
|
onClick,
|
|
10290
11612
|
"aria-label": t("show_list"),
|
|
10291
|
-
children: /* @__PURE__ */ jsx(
|
|
11613
|
+
children: /* @__PURE__ */ jsx(PanelLeftOpenIcon, { size: iconSize.smallest })
|
|
10292
11614
|
})
|
|
10293
11615
|
});
|
|
10294
11616
|
}
|
|
@@ -10322,7 +11644,7 @@ function JsonPreviewBinding({ values }) {
|
|
|
10322
11644
|
}
|
|
10323
11645
|
//#endregion
|
|
10324
11646
|
//#region src/components/EntityInspector.tsx
|
|
10325
|
-
var EntityHistoryView = lazy(() => import("./history-
|
|
11647
|
+
var EntityHistoryView = lazy(() => import("./history-D5SKygpJ.js").then((m) => ({ default: m.EntityHistoryView })));
|
|
10326
11648
|
/**
|
|
10327
11649
|
* Raw values and revision history, as an inspector rather than as tabs.
|
|
10328
11650
|
*
|
|
@@ -10340,13 +11662,13 @@ function EntityInspector({ tab, onTabChange, onClose, collection, entity, formCo
|
|
|
10340
11662
|
useEffect(() => {
|
|
10341
11663
|
if (!open) return;
|
|
10342
11664
|
const onKeyDown = (e) => {
|
|
10343
|
-
if (e.key
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
11665
|
+
if (e.key !== "Escape") return;
|
|
11666
|
+
if (document.querySelector("[role=\"dialog\"][data-state=\"open\"]")) return;
|
|
11667
|
+
e.stopPropagation();
|
|
11668
|
+
onClose();
|
|
10347
11669
|
};
|
|
10348
|
-
|
|
10349
|
-
return () =>
|
|
11670
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
11671
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
10350
11672
|
}, [open, onClose]);
|
|
10351
11673
|
if (!open) return null;
|
|
10352
11674
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -10433,6 +11755,15 @@ function useEntityDisplayTitle({ collection, entity, values, status }) {
|
|
|
10433
11755
|
* Shared tab value constants used by EditViewBinding and DetailViewBinding.
|
|
10434
11756
|
*/
|
|
10435
11757
|
var MAIN_TAB_VALUE = "__main_##Q$SC^#S6";
|
|
11758
|
+
/**
|
|
11759
|
+
* Every tab in that row, a custom `tabComponent` included.
|
|
11760
|
+
*
|
|
11761
|
+
* `h-full` over the variant's `h-9`: the row is 40px and the tab 36px, so a
|
|
11762
|
+
* centred tab floated ~2px clear of the row's bottom border and the active
|
|
11763
|
+
* tab's underline stopped short of the record it opens. Paired with
|
|
11764
|
+
* `items-stretch` on the Tabs root, which the variant leaves centred.
|
|
11765
|
+
*/
|
|
11766
|
+
var ENTITY_VIEW_TAB_CLASS = "h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800";
|
|
10436
11767
|
//#endregion
|
|
10437
11768
|
//#region src/components/DetailViewBinding.tsx
|
|
10438
11769
|
function DetailViewBinding({ entityId, ...props }) {
|
|
@@ -10458,7 +11789,7 @@ function DetailViewBinding({ entityId, ...props }) {
|
|
|
10458
11789
|
children: content
|
|
10459
11790
|
});
|
|
10460
11791
|
}
|
|
10461
|
-
function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, entity, dataLoading, layout = "full_screen", barActions, barActionsStart, onShowList }) {
|
|
11792
|
+
function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onTabChange, onEditClick, onCloseRequest, entity, dataLoading, layout = "full_screen", barActions, barActionsStart, onShowList }) {
|
|
10462
11793
|
const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
|
|
10463
11794
|
const { t } = useTranslation();
|
|
10464
11795
|
const context = useRebaseContext();
|
|
@@ -10494,7 +11825,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10494
11825
|
const target = val === "edit" ? MAIN_TAB_VALUE : val;
|
|
10495
11826
|
if (target !== selectedTab) setSelectedTab(target);
|
|
10496
11827
|
}, [selectedTabProp, defaultSelectedView]);
|
|
10497
|
-
const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.
|
|
11828
|
+
const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.hideFromEntityViews);
|
|
10498
11829
|
const subcollections = childViews.map((v) => v.collection);
|
|
10499
11830
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
10500
11831
|
const customViews = collection.entityViews ?? [];
|
|
@@ -10645,9 +11976,9 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10645
11976
|
]);
|
|
10646
11977
|
const propertyDetailView = () => {
|
|
10647
11978
|
if (FormViewBuilder && usedEntity) return /* @__PURE__ */ jsx("div", {
|
|
10648
|
-
className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
|
|
11979
|
+
className: "@container/form flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
|
|
10649
11980
|
children: /* @__PURE__ */ jsx("div", {
|
|
10650
|
-
className: cls("w-full max-w-3xl 2xl:max-w-4xl flex flex-col", layout === "dialog" ? "pt-5 pb-12 px-6 sm:px-8" : "pt-6 pb-16 px-5 sm:px-8"),
|
|
11981
|
+
className: cls("w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col", layout === "dialog" ? "pt-5 pb-12 px-6 sm:px-8" : "pt-6 pb-16 px-5 sm:px-8"),
|
|
10651
11982
|
children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
|
|
10652
11983
|
fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
|
|
10653
11984
|
children: /* @__PURE__ */ jsx(FormViewBuilder, {
|
|
@@ -10673,7 +12004,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10673
12004
|
const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
|
|
10674
12005
|
const icon = getIcon(subcollection.icon, void 0, void 0, "smallest");
|
|
10675
12006
|
return /* @__PURE__ */ jsx(Tab, {
|
|
10676
|
-
className: "text-sm min-w-[90px]",
|
|
12007
|
+
className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
|
|
10677
12008
|
value: subcollection.slug,
|
|
10678
12009
|
children: /* @__PURE__ */ jsxs("span", {
|
|
10679
12010
|
className: "flex items-center gap-1.5",
|
|
@@ -10684,7 +12015,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10684
12015
|
const customViewTabsStart = resolvedEntityViews.filter((view) => view.position === "start").map((view) => {
|
|
10685
12016
|
const icon = getIcon(view.icon, void 0, void 0, "smallest");
|
|
10686
12017
|
return /* @__PURE__ */ jsx(Tab, {
|
|
10687
|
-
className: !view.tabComponent
|
|
12018
|
+
className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
|
|
10688
12019
|
value: view.key,
|
|
10689
12020
|
children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
|
|
10690
12021
|
className: "flex items-center gap-1.5",
|
|
@@ -10695,7 +12026,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10695
12026
|
const customViewTabsEnd = resolvedEntityViews.filter((view) => !view.position || view.position === "end").map((view) => {
|
|
10696
12027
|
const icon = getIcon(view.icon, void 0, void 0, "smallest");
|
|
10697
12028
|
return /* @__PURE__ */ jsx(Tab, {
|
|
10698
|
-
className: !view.tabComponent
|
|
12029
|
+
className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
|
|
10699
12030
|
value: view.key,
|
|
10700
12031
|
children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
|
|
10701
12032
|
className: "flex items-center gap-1.5",
|
|
@@ -10740,6 +12071,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10740
12071
|
dirty: false,
|
|
10741
12072
|
saving: false,
|
|
10742
12073
|
onBack: backToCollection,
|
|
12074
|
+
onClose: onCloseRequest,
|
|
10743
12075
|
onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
|
|
10744
12076
|
onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
|
|
10745
12077
|
externalLink: usedEntity ? customizationController?.entityLinkBuilder?.({ entity: usedEntity }) : void 0,
|
|
@@ -10762,9 +12094,9 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10762
12094
|
] })
|
|
10763
12095
|
}),
|
|
10764
12096
|
hasAdditionalViews && /* @__PURE__ */ jsx("div", {
|
|
10765
|
-
className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", defaultBorderMixin),
|
|
12097
|
+
className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", "bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
|
|
10766
12098
|
children: /* @__PURE__ */ jsxs(Tabs, {
|
|
10767
|
-
className: "!w-full",
|
|
12099
|
+
className: "!w-full items-stretch",
|
|
10768
12100
|
innerClassName: "h-full",
|
|
10769
12101
|
variant: "boxy",
|
|
10770
12102
|
value: activeTab,
|
|
@@ -10774,6 +12106,7 @@ function DetailViewBindingInner({ path, entityId, selectedTab: selectedTabProp,
|
|
|
10774
12106
|
children: [
|
|
10775
12107
|
/* @__PURE__ */ jsx(Tab, {
|
|
10776
12108
|
value: "__main_##Q$SC^#S6",
|
|
12109
|
+
className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
|
|
10777
12110
|
children: /* @__PURE__ */ jsxs("span", {
|
|
10778
12111
|
className: "flex items-center gap-1.5",
|
|
10779
12112
|
children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
|
|
@@ -10909,12 +12242,14 @@ function isListShortcutTarget(target, detailPanel) {
|
|
|
10909
12242
|
* The list only has to identify a record — a title, a subtitle and a status
|
|
10910
12243
|
* chip — while the detail beside it carries the whole record and its rail. At
|
|
10911
12244
|
* 30% the list was taking width from the side that had four columns of fields
|
|
10912
|
-
* to lay out.
|
|
12245
|
+
* to lay out; 26% still was, by about the width of one field.
|
|
10913
12246
|
*
|
|
10914
12247
|
* A default, not a constraint: {@link savePanelSize} remembers the drag per
|
|
10915
|
-
* collection, so anyone who has already sized this list keeps their size.
|
|
12248
|
+
* collection, so anyone who has already sized this list keeps their size. The
|
|
12249
|
+
* drag floor is `minPanelSizePx`, and it holds below ~1100px of split — at that
|
|
12250
|
+
* width this percentage is already the floor.
|
|
10916
12251
|
*/
|
|
10917
|
-
var DEFAULT_PANEL_SIZE =
|
|
12252
|
+
var DEFAULT_PANEL_SIZE = 22;
|
|
10918
12253
|
function getSavedPanelSize(path) {
|
|
10919
12254
|
try {
|
|
10920
12255
|
const saved = localStorage.getItem(`${PANEL_SIZE_KEY}_${path}`);
|
|
@@ -10957,7 +12292,10 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
|
|
|
10957
12292
|
const setDirty = useCallback((modified) => {
|
|
10958
12293
|
dirty.current = modified;
|
|
10959
12294
|
}, []);
|
|
12295
|
+
const previousEntityId = useRef(selectedEntityId);
|
|
10960
12296
|
useEffect(() => {
|
|
12297
|
+
if (previousEntityId.current === selectedEntityId) return;
|
|
12298
|
+
previousEntityId.current = selectedEntityId;
|
|
10961
12299
|
dirty.current = false;
|
|
10962
12300
|
}, [selectedEntityId]);
|
|
10963
12301
|
const blocker = useNavigationBlocker(useCallback(({ currentLocation, nextLocation }) => {
|
|
@@ -10970,9 +12308,10 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
|
|
|
10970
12308
|
return true;
|
|
10971
12309
|
}, []));
|
|
10972
12310
|
const [panelSize, setPanelSize] = useState(() => getSavedPanelSize(path));
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
12311
|
+
const handlePanelSizeChange = useCallback((size) => {
|
|
12312
|
+
setPanelSize(size);
|
|
12313
|
+
savePanelSize(path, size);
|
|
12314
|
+
}, [path]);
|
|
10976
12315
|
const [renderedEntityId, setRenderedEntityId] = useState(selectedEntityId);
|
|
10977
12316
|
const [animationPhase, setAnimationPhase] = useState(selectedEntityId !== void 0 ? "entered" : "idle");
|
|
10978
12317
|
const animationTimer = useRef(null);
|
|
@@ -11100,6 +12439,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
|
|
|
11100
12439
|
parentEntityIds: usedParentEntityIds,
|
|
11101
12440
|
selectedTab,
|
|
11102
12441
|
layout: "split",
|
|
12442
|
+
onCloseRequest: handleCloseDetail,
|
|
11103
12443
|
onEditClick: () => {
|
|
11104
12444
|
navigate(buildUrl(`${path}/${renderedEntityId}/edit`));
|
|
11105
12445
|
},
|
|
@@ -11115,6 +12455,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
|
|
|
11115
12455
|
parentEntityIds: usedParentEntityIds,
|
|
11116
12456
|
selectedTab,
|
|
11117
12457
|
layout: "split",
|
|
12458
|
+
onCloseRequest: handleCloseDetail,
|
|
11118
12459
|
onValuesModified: setDirty,
|
|
11119
12460
|
onSaved: () => {
|
|
11120
12461
|
setDirty(false);
|
|
@@ -11150,7 +12491,7 @@ function SplitListView({ collection, tableController, onEntityClick: externalOnE
|
|
|
11150
12491
|
secondPanel: detailPanel,
|
|
11151
12492
|
showSecondPanel: isDetailVisible,
|
|
11152
12493
|
panelSizePercent: animationPhase === "entering" ? 100 : panelSize,
|
|
11153
|
-
onPanelSizeChange:
|
|
12494
|
+
onPanelSizeChange: handlePanelSizeChange,
|
|
11154
12495
|
minPanelSizePx: 240
|
|
11155
12496
|
}), /* @__PURE__ */ jsx(UnsavedChangesDialog, {
|
|
11156
12497
|
open: blocker?.state === "blocked",
|
|
@@ -11297,6 +12638,17 @@ function BoardCardBindingInner({ item, isDragging, isGroupedOver, style, collect
|
|
|
11297
12638
|
className: "text-[10px] text-surface-400 dark:text-surface-500 shrink-0",
|
|
11298
12639
|
children: ["+", slots.relations.reduce((acc, r) => acc + Math.max(0, r.totalCount - r.items.length), 0)]
|
|
11299
12640
|
})]
|
|
12641
|
+
}),
|
|
12642
|
+
slots.tags && /* @__PURE__ */ jsx("div", {
|
|
12643
|
+
className: "flex items-center gap-1 mt-1 flex-wrap",
|
|
12644
|
+
children: /* @__PURE__ */ jsx(TagChips, {
|
|
12645
|
+
slot: slots.tags,
|
|
12646
|
+
max: 3
|
|
12647
|
+
})
|
|
12648
|
+
}),
|
|
12649
|
+
slots.date && /* @__PURE__ */ jsx("div", {
|
|
12650
|
+
className: "text-[10px] text-surface-500 dark:text-surface-400 mt-1",
|
|
12651
|
+
children: slots.date.formatted
|
|
11300
12652
|
})
|
|
11301
12653
|
]
|
|
11302
12654
|
})]
|
|
@@ -11414,6 +12766,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
|
|
|
11414
12766
|
}, []);
|
|
11415
12767
|
const subscribeToColumn = useCallback((column, itemCount) => {
|
|
11416
12768
|
if (isCleaningUpRef.current) return;
|
|
12769
|
+
const requestedCount = Math.min(itemCount, MAX_LIST_LIMIT);
|
|
11417
12770
|
const currentDataClient = dataClientRef.current;
|
|
11418
12771
|
const currentCollection = collectionRef.current;
|
|
11419
12772
|
const currentContext = contextRef.current;
|
|
@@ -11543,7 +12896,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
|
|
|
11543
12896
|
const includeParams = getRelationIncludeParams(currentCollection);
|
|
11544
12897
|
const fetchOnce = () => accessor.find({
|
|
11545
12898
|
where: whereFilter,
|
|
11546
|
-
limit:
|
|
12899
|
+
limit: requestedCount,
|
|
11547
12900
|
orderBy: orderByParam,
|
|
11548
12901
|
include: includeParams,
|
|
11549
12902
|
searchString: currentSearchString
|
|
@@ -11569,7 +12922,7 @@ function useBoardDataController({ fullPath, collection, columnProperty, columns,
|
|
|
11569
12922
|
let liveDataReceived = false;
|
|
11570
12923
|
const unsubscribe = accessor.listen({
|
|
11571
12924
|
where: whereFilter,
|
|
11572
|
-
limit:
|
|
12925
|
+
limit: requestedCount,
|
|
11573
12926
|
orderBy: orderByParam,
|
|
11574
12927
|
include: includeParams,
|
|
11575
12928
|
searchString: currentSearchString
|
|
@@ -12152,7 +13505,7 @@ function CollectionBoardViewBinding({ collection, tableController, fullPath, par
|
|
|
12152
13505
|
analyticsController.onAnalyticsEvent?.("kanban_backfill_order", { path: fullPath });
|
|
12153
13506
|
setBackfillLoading(true);
|
|
12154
13507
|
try {
|
|
12155
|
-
const entitiesToUpdate = (await dataClient.collection(fullPath).find({ limit:
|
|
13508
|
+
const entitiesToUpdate = (await dataClient.collection(fullPath).find({ limit: MAX_LIST_LIMIT })).data.filter((entity) => !isValidOrderKey(entity.values?.[orderProperty]));
|
|
12156
13509
|
const keys = generateNKeysBetween(null, null, entitiesToUpdate.length, ORDER_KEY_DIGITS);
|
|
12157
13510
|
const updates = [];
|
|
12158
13511
|
entitiesToUpdate.forEach((entity, index) => {
|
|
@@ -12392,10 +13745,10 @@ var ALL_VIEW_MODES = [
|
|
|
12392
13745
|
function ViewModeToggle({ viewMode = "table", onViewModeChange, enabledViews = ALL_VIEW_MODES, size, onSizeChanged, open, onOpenChange, kanbanPropertyOptions, selectedKanbanProperty, onKanbanPropertyChange }) {
|
|
12393
13746
|
const { t } = useTranslation();
|
|
12394
13747
|
const getViewModeIcon = () => {
|
|
12395
|
-
if (viewMode === "kanban") return /* @__PURE__ */ jsx(KanbanIcon, { size: iconSize.
|
|
12396
|
-
if (viewMode === "cards") return /* @__PURE__ */ jsx(LayoutGridIcon, { size: iconSize.
|
|
12397
|
-
if (viewMode === "table") return /* @__PURE__ */ jsx(TableIcon, { size: iconSize.
|
|
12398
|
-
return /* @__PURE__ */ jsx(ListIcon, { size: iconSize.
|
|
13748
|
+
if (viewMode === "kanban") return /* @__PURE__ */ jsx(KanbanIcon, { size: iconSize.small });
|
|
13749
|
+
if (viewMode === "cards") return /* @__PURE__ */ jsx(LayoutGridIcon, { size: iconSize.small });
|
|
13750
|
+
if (viewMode === "table") return /* @__PURE__ */ jsx(TableIcon, { size: iconSize.small });
|
|
13751
|
+
return /* @__PURE__ */ jsx(ListIcon, { size: iconSize.small });
|
|
12399
13752
|
};
|
|
12400
13753
|
const getViewModeName = () => {
|
|
12401
13754
|
if (viewMode === "kanban") return t("board");
|
|
@@ -12664,12 +14017,13 @@ function addRecentId(collectionId, id) {
|
|
|
12664
14017
|
return newRecentIds;
|
|
12665
14018
|
}
|
|
12666
14019
|
function saveSearchedIdsLocally(collectionId, ids) {
|
|
12667
|
-
|
|
14020
|
+
writeStoredJson("recent_id_searches::" + collectionId, ids);
|
|
12668
14021
|
}
|
|
12669
14022
|
function getRecentIds(collectionId) {
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
14023
|
+
return readStoredJson("recent_id_searches::" + collectionId, {
|
|
14024
|
+
fallback: [],
|
|
14025
|
+
accept: isArrayValue
|
|
14026
|
+
});
|
|
12673
14027
|
}
|
|
12674
14028
|
//#endregion
|
|
12675
14029
|
//#region src/components/admin/CreationResultDialog.tsx
|
|
@@ -13346,7 +14700,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
|
|
|
13346
14700
|
const property = propertyKey && getPropertyInPath$1(collection?.properties ?? {}, propertyKey);
|
|
13347
14701
|
const fieldProps = propertyKey && property ? {
|
|
13348
14702
|
propertyKey,
|
|
13349
|
-
disabled: isSubmitting || isReadOnly(property) ||
|
|
14703
|
+
disabled: isSubmitting || isReadOnly(property) || isDisabled(property),
|
|
13350
14704
|
property,
|
|
13351
14705
|
includeDescription: false,
|
|
13352
14706
|
underlyingValueHasChanged: false,
|
|
@@ -13433,7 +14787,7 @@ function PopupFormFieldInternal({ tableKey, entityId, customFieldValidator, prop
|
|
|
13433
14787
|
}
|
|
13434
14788
|
//#endregion
|
|
13435
14789
|
//#region src/components/ClearFilterSortButton.tsx
|
|
13436
|
-
function ClearFilterSortButton({ tableController, enabled }) {
|
|
14790
|
+
function ClearFilterSortButton({ tableController, enabled, compact }) {
|
|
13437
14791
|
const { t } = useTranslation();
|
|
13438
14792
|
if (!enabled) return null;
|
|
13439
14793
|
const filterIsSet = !!tableController.filterValues && Object.keys(tableController.filterValues).length > 0;
|
|
@@ -13445,15 +14799,14 @@ function ClearFilterSortButton({ tableController, enabled }) {
|
|
|
13445
14799
|
else label = t("clear_sort");
|
|
13446
14800
|
return /* @__PURE__ */ jsx(Tooltip, {
|
|
13447
14801
|
title: label,
|
|
13448
|
-
children: /* @__PURE__ */ jsx(
|
|
14802
|
+
children: /* @__PURE__ */ jsx(IconButton, {
|
|
13449
14803
|
size: "small",
|
|
13450
|
-
variant: "text",
|
|
13451
14804
|
"aria-label": label,
|
|
13452
14805
|
onClick: () => {
|
|
13453
14806
|
tableController.clearFilter?.();
|
|
13454
14807
|
tableController.setSortBy?.(void 0);
|
|
13455
14808
|
},
|
|
13456
|
-
children: /* @__PURE__ */ jsx(FilterXIcon, { size: iconSize.small })
|
|
14809
|
+
children: /* @__PURE__ */ jsx(FilterXIcon, { size: compact ? iconSize.smallest : iconSize.small })
|
|
13457
14810
|
})
|
|
13458
14811
|
});
|
|
13459
14812
|
}
|
|
@@ -13811,7 +15164,7 @@ function FilterPresetsButton({ filterPresets, tableController, compact }) {
|
|
|
13811
15164
|
* is one click rather than a trip through a direction selector; clicking a
|
|
13812
15165
|
* different one starts it ascending. The popover stays open through both.
|
|
13813
15166
|
*/
|
|
13814
|
-
function SortButton({ tableController, properties }) {
|
|
15167
|
+
function SortButton({ tableController, properties, compact }) {
|
|
13815
15168
|
const { t } = useTranslation();
|
|
13816
15169
|
const [open, setOpen] = useState(false);
|
|
13817
15170
|
const options = useMemo(() => getSortablePropertyOptions(properties), [properties]);
|
|
@@ -13837,6 +15190,7 @@ function SortButton({ tableController, properties }) {
|
|
|
13837
15190
|
if (!setSortBy || options.length === 0) return null;
|
|
13838
15191
|
const activeOption = options.find((option) => option.key === activeKey);
|
|
13839
15192
|
const label = activeOption ? `${t("sort_by")}: ${activeOption.title}` : t("sort");
|
|
15193
|
+
const arrowSize = compact ? iconSize.smallest : iconSize.small;
|
|
13840
15194
|
return /* @__PURE__ */ jsx(Popover, {
|
|
13841
15195
|
open,
|
|
13842
15196
|
onOpenChange: setOpen,
|
|
@@ -13848,7 +15202,7 @@ function SortButton({ tableController, properties }) {
|
|
|
13848
15202
|
"aria-label": label,
|
|
13849
15203
|
title: label,
|
|
13850
15204
|
className: cls(activeOption && "text-primary"),
|
|
13851
|
-
children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size:
|
|
15205
|
+
children: activeDirection === "desc" ? /* @__PURE__ */ jsx(ArrowDownIcon, { size: arrowSize }) : activeDirection === "asc" ? /* @__PURE__ */ jsx(ArrowUpIcon, { size: arrowSize }) : /* @__PURE__ */ jsx(ArrowUpDownIcon, { size: arrowSize })
|
|
13852
15206
|
}),
|
|
13853
15207
|
children: /* @__PURE__ */ jsxs("div", {
|
|
13854
15208
|
className: "py-2 min-w-[240px] flex flex-col",
|
|
@@ -13945,6 +15299,7 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
|
|
|
13945
15299
|
children: /* @__PURE__ */ jsx(XIcon, { size: iconSize.smallest })
|
|
13946
15300
|
})
|
|
13947
15301
|
}, "close_tooltip");
|
|
15302
|
+
const iconOnlyToolbar = !largeLayout || Boolean(compact);
|
|
13948
15303
|
const filtersButton = resolvedProperties && tableController.setFilterValues && /* @__PURE__ */ jsx(Tooltip, {
|
|
13949
15304
|
title: t("filters"),
|
|
13950
15305
|
children: /* @__PURE__ */ jsx(Badge, {
|
|
@@ -13967,7 +15322,8 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
|
|
|
13967
15322
|
}, "filters_tooltip");
|
|
13968
15323
|
const sortButton = resolvedProperties && (viewMode === "list" || viewMode === "cards") && /* @__PURE__ */ jsx(SortButton, {
|
|
13969
15324
|
tableController,
|
|
13970
|
-
properties: resolvedProperties
|
|
15325
|
+
properties: resolvedProperties,
|
|
15326
|
+
compact: iconOnlyToolbar
|
|
13971
15327
|
}, "sort_button");
|
|
13972
15328
|
const filterPresetsButton = !compact && collection.filterPresets?.length ? /* @__PURE__ */ jsx(FilterPresetsButton, {
|
|
13973
15329
|
filterPresets: collection.filterPresets,
|
|
@@ -13981,6 +15337,7 @@ function CollectionViewStartActions({ collection, relativePath, parentCollection
|
|
|
13981
15337
|
sortButton,
|
|
13982
15338
|
/* @__PURE__ */ jsx(ClearFilterSortButton, {
|
|
13983
15339
|
tableController,
|
|
15340
|
+
compact: iconOnlyToolbar,
|
|
13984
15341
|
enabled: !collection.fixedFilter
|
|
13985
15342
|
}, "clear_filter"),
|
|
13986
15343
|
filterPresetsButton
|
|
@@ -14139,16 +15496,16 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
|
|
|
14139
15496
|
viewMode
|
|
14140
15497
|
});
|
|
14141
15498
|
useEffect(() => {
|
|
14142
|
-
if (!urlView && viewMode !== defaultViewMode) setSearchParams((
|
|
14143
|
-
const next = new URLSearchParams(
|
|
15499
|
+
if (!urlView && viewMode !== defaultViewMode) setSearchParams(() => {
|
|
15500
|
+
const next = new URLSearchParams(window.location.search);
|
|
14144
15501
|
next.set(VIEW_MODE_PARAM, viewMode);
|
|
14145
15502
|
return next;
|
|
14146
15503
|
}, { replace: true });
|
|
14147
15504
|
}, []);
|
|
14148
15505
|
const setViewMode = useCallback((newMode) => {
|
|
14149
15506
|
setViewModeState(newMode);
|
|
14150
|
-
setSearchParams((
|
|
14151
|
-
const next = new URLSearchParams(
|
|
15507
|
+
setSearchParams(() => {
|
|
15508
|
+
const next = new URLSearchParams(window.location.search);
|
|
14152
15509
|
if (newMode === defaultViewMode) next.delete(VIEW_MODE_PARAM);
|
|
14153
15510
|
else next.set(VIEW_MODE_PARAM, newMode);
|
|
14154
15511
|
return next;
|
|
@@ -14452,15 +15809,17 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
|
|
|
14452
15809
|
if (!property) property = getPropertyInPath$1(resolvedCollection.properties, propertyKey);
|
|
14453
15810
|
return property ?? null;
|
|
14454
15811
|
}, [collection.properties, resolvedCollection.properties]);
|
|
14455
|
-
const
|
|
15812
|
+
const collectionWithLocalOrder = useMemo(() => {
|
|
14456
15813
|
if (localPropertiesOrder && localPropertiesOrder !== resolvedCollection.propertiesOrder) return {
|
|
14457
15814
|
...resolvedCollection,
|
|
14458
15815
|
propertiesOrder: localPropertiesOrder
|
|
14459
15816
|
};
|
|
14460
15817
|
return resolvedCollection;
|
|
14461
|
-
}, [resolvedCollection, localPropertiesOrder])
|
|
15818
|
+
}, [resolvedCollection, localPropertiesOrder]);
|
|
15819
|
+
const displayedColumnIds = useColumnIds(collectionWithLocalOrder, true);
|
|
14462
15820
|
const additionalFields = useMemo(() => {
|
|
14463
|
-
const
|
|
15821
|
+
const redundant = getRedundantChildViewColumnIds(collectionWithLocalOrder);
|
|
15822
|
+
const subcollectionColumns = getEntityChildViews(collection).map((v) => v.collection).filter((child) => !redundant.has(getSubcollectionColumnId(child))).map((subcollection) => {
|
|
14464
15823
|
return {
|
|
14465
15824
|
key: getSubcollectionColumnId(subcollection),
|
|
14466
15825
|
name: subcollection.name,
|
|
@@ -14488,6 +15847,7 @@ var CollectionViewBindingInner = React.memo(function CollectionViewBindingInner(
|
|
|
14488
15847
|
return [...collection.additionalFields ?? EMPTY_ARRAY, ...subcollectionColumns];
|
|
14489
15848
|
}, [
|
|
14490
15849
|
collection,
|
|
15850
|
+
collectionWithLocalOrder,
|
|
14491
15851
|
path,
|
|
14492
15852
|
sidePanelController
|
|
14493
15853
|
]);
|
|
@@ -14993,6 +16353,7 @@ function EntityIdHeaderWidget({ collection, path, idPath, openEntityMode }) {
|
|
|
14993
16353
|
className: "flex p-2 w-full gap-2",
|
|
14994
16354
|
children: [/* @__PURE__ */ jsx(TextField, {
|
|
14995
16355
|
autoFocus: openPopup,
|
|
16356
|
+
"aria-label": t("find_entity_by_id"),
|
|
14996
16357
|
placeholder: t("find_entity_by_id"),
|
|
14997
16358
|
size: "small",
|
|
14998
16359
|
onChange: (e) => {
|
|
@@ -15036,6 +16397,137 @@ function EntityIdHeaderWidget({ collection, path, idPath, openEntityMode }) {
|
|
|
15036
16397
|
});
|
|
15037
16398
|
}
|
|
15038
16399
|
//#endregion
|
|
16400
|
+
//#region src/form/form_utils.ts
|
|
16401
|
+
function extractTouchedValues(values, touched) {
|
|
16402
|
+
let acc = {};
|
|
16403
|
+
if (!touched || typeof touched !== "object") return acc;
|
|
16404
|
+
Object.entries(touched).forEach(([key, value]) => {
|
|
16405
|
+
if (value) acc = setIn(acc, key, getIn(values, key));
|
|
16406
|
+
});
|
|
16407
|
+
return acc;
|
|
16408
|
+
}
|
|
16409
|
+
/**
|
|
16410
|
+
* Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
|
|
16411
|
+
* This prevents ghost containers created by `setIn` intermediate path construction
|
|
16412
|
+
* (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
|
|
16413
|
+
* from falsely triggering the unsaved local changes indicator.
|
|
16414
|
+
*/
|
|
16415
|
+
/**
|
|
16416
|
+
* Check if a value is semantically empty (null, undefined, or empty string).
|
|
16417
|
+
*/
|
|
16418
|
+
function isSemanticEmpty(v) {
|
|
16419
|
+
return v === null || v === void 0 || v === "";
|
|
16420
|
+
}
|
|
16421
|
+
function removeEmptyContainers(obj) {
|
|
16422
|
+
if (Array.isArray(obj)) return obj.map(removeEmptyContainers);
|
|
16423
|
+
if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
|
|
16424
|
+
const result = {};
|
|
16425
|
+
for (const key of Object.keys(obj)) {
|
|
16426
|
+
const cleaned = removeEmptyContainers(obj[key]);
|
|
16427
|
+
if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned) && Object.getPrototypeOf(cleaned) === Object.prototype && Object.keys(cleaned).length === 0) continue;
|
|
16428
|
+
result[key] = cleaned;
|
|
16429
|
+
}
|
|
16430
|
+
if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) return {};
|
|
16431
|
+
return result;
|
|
16432
|
+
}
|
|
16433
|
+
return obj;
|
|
16434
|
+
}
|
|
16435
|
+
function getChanges(source, comparison) {
|
|
16436
|
+
const changes = {};
|
|
16437
|
+
if (!source) return {};
|
|
16438
|
+
if (!comparison) return source;
|
|
16439
|
+
const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(comparison)]));
|
|
16440
|
+
for (const key of allKeys) {
|
|
16441
|
+
const sourceValue = source[key];
|
|
16442
|
+
const comparisonValue = comparison[key];
|
|
16443
|
+
if (deepEqual(sourceValue, comparisonValue)) continue;
|
|
16444
|
+
const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
|
|
16445
|
+
if (comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key) && !sourceHasKey) changes[key] = void 0;
|
|
16446
|
+
else if (Array.isArray(sourceValue)) {
|
|
16447
|
+
const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
|
|
16448
|
+
if (sourceValue.length !== comparisonArray.length) {
|
|
16449
|
+
changes[key] = sourceValue;
|
|
16450
|
+
continue;
|
|
16451
|
+
}
|
|
16452
|
+
if (sourceValue.some((item, index) => !deepEqual(item, comparisonArray[index]))) changes[key] = sourceValue;
|
|
16453
|
+
} else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
|
|
16454
|
+
const nestedChanges = getChanges(sourceValue, comparisonValue);
|
|
16455
|
+
if (Object.keys(nestedChanges).length > 0) changes[key] = nestedChanges;
|
|
16456
|
+
} else changes[key] = sourceValue;
|
|
16457
|
+
}
|
|
16458
|
+
return changes;
|
|
16459
|
+
}
|
|
16460
|
+
/**
|
|
16461
|
+
* What the local-changes backup still has to offer, given what the form is
|
|
16462
|
+
* already showing — the answer the "unsaved local changes" banner is asking for.
|
|
16463
|
+
* `undefined` means there is nothing left to apply, and no banner.
|
|
16464
|
+
*
|
|
16465
|
+
* Measured against what the form *opens showing*, not against the stored
|
|
16466
|
+
* record. The banner exists to offer a draft the form is not already displaying,
|
|
16467
|
+
* and an edit carried across a change of layout is in both the backup and the
|
|
16468
|
+
* form: measuring against the stored values alone offered to restore the values
|
|
16469
|
+
* already on screen, over an edit the user had never left.
|
|
16470
|
+
*/
|
|
16471
|
+
function getUnappliedLocalChanges(backupValues, openingValues) {
|
|
16472
|
+
const cleaned = removeEmptyContainers(getChanges(backupValues, openingValues));
|
|
16473
|
+
if (!cleaned || typeof cleaned !== "object" || Object.keys(cleaned).length === 0) return;
|
|
16474
|
+
return cleaned;
|
|
16475
|
+
}
|
|
16476
|
+
/**
|
|
16477
|
+
* What travels when the same record changes layout — the split's "hide list",
|
|
16478
|
+
* full screen's "show list", the side panel's "open full screen". One mounted
|
|
16479
|
+
* form is replaced by another showing the same record, so the edit in progress
|
|
16480
|
+
* has to be handed over; left to the local-changes backup alone, the new form
|
|
16481
|
+
* met it as a draft from a closed tab and raised the "unsaved local changes"
|
|
16482
|
+
* banner over changes the user had made a second earlier and never left.
|
|
16483
|
+
*
|
|
16484
|
+
* Returns `undefined` when there is nothing to carry, which is its own answer: a
|
|
16485
|
+
* record nobody edited must open in the next layout exactly as clean as it was.
|
|
16486
|
+
*
|
|
16487
|
+
* Only what was edited here travels, keyed by `touched` — the same subset the
|
|
16488
|
+
* backup stores. Carrying the whole record instead marks every field touched in
|
|
16489
|
+
* the receiving form, which lights up every empty required field as an error and
|
|
16490
|
+
* writes the entire record back out as a "local change".
|
|
16491
|
+
*/
|
|
16492
|
+
function getEditHandoffValues({ status, dirty, values, touched, storedValues }) {
|
|
16493
|
+
if (!values) return void 0;
|
|
16494
|
+
if (status === "new" || status === "copy") return Object.keys(values).length > 0 ? values : void 0;
|
|
16495
|
+
if (!dirty) return void 0;
|
|
16496
|
+
const touchedValues = removeEmptyContainers(extractTouchedValues(values, touched ?? {}));
|
|
16497
|
+
if (touchedValues && Object.keys(touchedValues).length > 0) return touchedValues;
|
|
16498
|
+
const changes = getChanges(values, storedValues ?? {});
|
|
16499
|
+
return Object.keys(changes).length > 0 ? changes : void 0;
|
|
16500
|
+
}
|
|
16501
|
+
function getInitialEntityValues(authController, collection, path, status, entity, propertyConfigs) {
|
|
16502
|
+
const properties = collection.properties;
|
|
16503
|
+
if ((status === "existing" || status === "copy") && entity) {
|
|
16504
|
+
let values;
|
|
16505
|
+
if (!collection.alwaysApplyDefaultValues) values = entity.values ?? getDefaultValuesFor(properties);
|
|
16506
|
+
else values = mergeDeep(getDefaultValuesFor(properties), entity.values ?? {});
|
|
16507
|
+
if (status === "copy") {
|
|
16508
|
+
const result = { ...values };
|
|
16509
|
+
for (const [key, property] of Object.entries(properties)) if (property && "isId" in property && property.isId) delete result[key];
|
|
16510
|
+
return result;
|
|
16511
|
+
}
|
|
16512
|
+
return values;
|
|
16513
|
+
} else if (status === "new") return getDefaultValuesFor(properties);
|
|
16514
|
+
else {
|
|
16515
|
+
console.error({
|
|
16516
|
+
status,
|
|
16517
|
+
entity
|
|
16518
|
+
});
|
|
16519
|
+
throw new Error("Form has not been initialised with the correct parameters");
|
|
16520
|
+
}
|
|
16521
|
+
}
|
|
16522
|
+
function zodToFormErrors(zodError) {
|
|
16523
|
+
let errors = {};
|
|
16524
|
+
for (const issue of zodError.issues) {
|
|
16525
|
+
const path = issue.path.join(".");
|
|
16526
|
+
if (path && !getIn(errors, path)) errors = setIn(errors, path, issue.message);
|
|
16527
|
+
}
|
|
16528
|
+
return errors;
|
|
16529
|
+
}
|
|
16530
|
+
//#endregion
|
|
15039
16531
|
//#region src/components/EditFormActions.tsx
|
|
15040
16532
|
/**
|
|
15041
16533
|
* The entity form's footer.
|
|
@@ -15278,7 +16770,17 @@ function EditViewBinding({ entityId, ...props }) {
|
|
|
15278
16770
|
databaseId: props.databaseId,
|
|
15279
16771
|
useCache: false
|
|
15280
16772
|
});
|
|
15281
|
-
const
|
|
16773
|
+
const carriesAsNew = props.copy || entityId === void 0;
|
|
16774
|
+
const handoffKey = carriesAsNew ? props.path + "#new" : props.path + "/" + entityId;
|
|
16775
|
+
const handoff = useRef(null);
|
|
16776
|
+
if (handoff.current?.key !== handoffKey) handoff.current = {
|
|
16777
|
+
key: handoffKey,
|
|
16778
|
+
values: getEntityFromMemoryCache(handoffKey)
|
|
16779
|
+
};
|
|
16780
|
+
useEffect(() => {
|
|
16781
|
+
removeEntityFromMemoryCache(handoffKey);
|
|
16782
|
+
}, [handoffKey]);
|
|
16783
|
+
const initialDirtyValues = carriesAsNew ? props.defaultValues ?? handoff.current.values : handoff.current.values;
|
|
15282
16784
|
const { canEdit: canEditHook } = usePermissions();
|
|
15283
16785
|
const [status, setStatus] = useState(props.copy ? "copy" : entityId ? "existing" : "new");
|
|
15284
16786
|
const canEdit = useMemo(() => {
|
|
@@ -15314,7 +16816,7 @@ function EditViewBinding({ entityId, ...props }) {
|
|
|
15314
16816
|
children: content
|
|
15315
16817
|
});
|
|
15316
16818
|
}
|
|
15317
|
-
function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, barActionsStart, onShowList, status, setStatus, formProps, canEdit }) {
|
|
16819
|
+
function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, collection, parentCollectionSlugs, parentEntityIds, onValuesModified, onSaved, onTabChange, navigateBack, onCloseRequest, entity, initialDirtyValues, dataLoading, layout = "side_panel", barActions, barActionsStart, onShowList, status, setStatus, formProps, canEdit }) {
|
|
15318
16820
|
const ResolvedFormActions = useComponentOverride("EditView.FormActions", EditFormActions);
|
|
15319
16821
|
const ResolvedEntityForm = useComponentOverride("Entity.Form", EntityFormBinding);
|
|
15320
16822
|
const ResolvedCollectionView = useComponentOverride("Collection.View", CollectionViewBinding);
|
|
@@ -15371,7 +16873,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15371
16873
|
const target = val === "edit" ? MAIN_TAB_VALUE : val;
|
|
15372
16874
|
if (target !== selectedTab) setSelectedTab(target);
|
|
15373
16875
|
}, [selectedTabProp, defaultSelectedView]);
|
|
15374
|
-
const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.
|
|
16876
|
+
const childViews = getAdminEntityChildViews(collection).filter((v) => !v.collection.hideFromEntityViews);
|
|
15375
16877
|
const subcollections = childViews.map((v) => v.collection);
|
|
15376
16878
|
const subcollectionsCount = subcollections?.length ?? 0;
|
|
15377
16879
|
const customViews = collection.entityViews ?? [];
|
|
@@ -15466,10 +16968,55 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15466
16968
|
const formex = formContext?.formex;
|
|
15467
16969
|
const hasFormErrors = Boolean(formex && Object.keys(formex.errors).length > 0 && formex.submitCount > 0);
|
|
15468
16970
|
const saveDisabled = Boolean(!formContext || formContext.disabled || formex?.isSubmitting || status === "existing" && !formex?.dirty);
|
|
16971
|
+
/**
|
|
16972
|
+
* Hand this record's edit in progress to the layout about to replace this
|
|
16973
|
+
* one — the split's "hide list", full screen's "show list", the side panel's
|
|
16974
|
+
* "open full screen". None of them is a navigation *away* from the record:
|
|
16975
|
+
* the same record is about to be shown by a different form, and what the
|
|
16976
|
+
* user has typed belongs to the record, not to the pane it was typed in.
|
|
16977
|
+
*
|
|
16978
|
+
* Every control that changes layout has to call this, and none of them is
|
|
16979
|
+
* blocked by the unsaved-changes prompt — the prompt asks about leaving, and
|
|
16980
|
+
* these do not leave. What used to happen instead is that the edit reached
|
|
16981
|
+
* the next layout only as its local-changes backup, which is the channel for
|
|
16982
|
+
* a draft left behind by a closed tab: the record reopened *clean*, with a
|
|
16983
|
+
* banner offering to apply changes the user had never walked away from.
|
|
16984
|
+
*/
|
|
16985
|
+
const carryEdit = useCallback(() => {
|
|
16986
|
+
const carried = formex ? getEditHandoffValues({
|
|
16987
|
+
status,
|
|
16988
|
+
dirty: formex.dirty,
|
|
16989
|
+
values: formex.values,
|
|
16990
|
+
touched: formex.touched,
|
|
16991
|
+
storedValues: usedEntity?.values
|
|
16992
|
+
}) : void 0;
|
|
16993
|
+
if (!carried) return;
|
|
16994
|
+
saveEntityToMemoryCache(status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId, carried);
|
|
16995
|
+
}, [
|
|
16996
|
+
formex,
|
|
16997
|
+
status,
|
|
16998
|
+
path,
|
|
16999
|
+
entityId,
|
|
17000
|
+
usedEntity
|
|
17001
|
+
]);
|
|
15469
17002
|
const [inspectorTab, setInspectorTab] = useState(null);
|
|
15470
17003
|
const [savedCount, setSavedCount] = useState(0);
|
|
15471
17004
|
const sideDialogContext = useSideDialogContext();
|
|
15472
|
-
|
|
17005
|
+
/**
|
|
17006
|
+
* Whether the save in flight was asked to dismiss the record afterwards.
|
|
17007
|
+
*
|
|
17008
|
+
* A ref, and for the reason {@link SideDialogs} keeps its own as one: the
|
|
17009
|
+
* flag is raised and the form submitted in a single click handler, and the
|
|
17010
|
+
* `onSaved` that has to act on it was captured before React could re-render.
|
|
17011
|
+
* As state it read one save late — the first "save and close" saved and
|
|
17012
|
+
* stayed open, and the *next* save closed the record.
|
|
17013
|
+
*
|
|
17014
|
+
* The side panel and the dialog use the side-dialog controller's flag,
|
|
17015
|
+
* which their own close path consumes; this one covers the layouts that
|
|
17016
|
+
* close by calling {@link onCloseRequest} instead.
|
|
17017
|
+
*/
|
|
17018
|
+
const pendingCloseRef = useRef(false);
|
|
17019
|
+
const canCloseAfterSave = layout === "side_panel" || layout === "dialog" || layout === "split" && Boolean(onCloseRequest);
|
|
15473
17020
|
const recordActions = useRecordActions({
|
|
15474
17021
|
collection,
|
|
15475
17022
|
path,
|
|
@@ -15540,9 +17087,9 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15540
17087
|
const FormViewBuilder = formViewConfig?.Builder ? resolveComponentRef(formViewConfig.Builder) : null;
|
|
15541
17088
|
formViewConfig?.includeActions;
|
|
15542
17089
|
const entityReadOnlyView = !canEdit && entity ? FormViewBuilder && readOnlyFormContext ? /* @__PURE__ */ jsx("div", {
|
|
15543
|
-
className: cls("flex-1 flex flex-row w-full overflow-y-auto justify-center", canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
|
|
17090
|
+
className: cls("@container/form flex-1 flex flex-row w-full overflow-y-auto justify-center", canEdit || !mainViewVisible || selectedSecondaryForm ? "hidden" : ""),
|
|
15544
17091
|
children: /* @__PURE__ */ jsx("div", {
|
|
15545
|
-
className: "w-full max-w-3xl 2xl:max-w-4xl flex flex-col pt-6 pb-16 px-5 sm:px-8",
|
|
17092
|
+
className: "w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col pt-6 pb-16 px-5 sm:px-8",
|
|
15546
17093
|
children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(Suspense, {
|
|
15547
17094
|
fallback: /* @__PURE__ */ jsx(CircularProgressCenter, {}),
|
|
15548
17095
|
children: /* @__PURE__ */ jsx(FormViewBuilder, {
|
|
@@ -15612,13 +17159,17 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15612
17159
|
};
|
|
15613
17160
|
onSaved?.(res);
|
|
15614
17161
|
formProps?.onSaved?.(res);
|
|
17162
|
+
if (pendingCloseRef.current) {
|
|
17163
|
+
pendingCloseRef.current = false;
|
|
17164
|
+
setTimeout(() => onCloseRequest?.(), 0);
|
|
17165
|
+
}
|
|
15615
17166
|
},
|
|
15616
17167
|
Builder: resolveComponentRef(selectedSecondaryForm?.Builder)
|
|
15617
17168
|
});
|
|
15618
17169
|
const subcollectionTabs = subcollections && subcollections.map((subcollection) => {
|
|
15619
17170
|
const icon = getIcon(subcollection.icon, void 0, void 0, "smallest");
|
|
15620
17171
|
return /* @__PURE__ */ jsx(Tab, {
|
|
15621
|
-
className: "text-sm min-w-[90px]",
|
|
17172
|
+
className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
|
|
15622
17173
|
value: subcollection.slug,
|
|
15623
17174
|
children: /* @__PURE__ */ jsxs("span", {
|
|
15624
17175
|
className: "flex items-center gap-1.5",
|
|
@@ -15629,7 +17180,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15629
17180
|
const customViewTabsStart = resolvedEntityViews.filter((view) => view.position === "start").map((view) => {
|
|
15630
17181
|
const icon = getIcon(view.icon, void 0, void 0, "smallest");
|
|
15631
17182
|
return /* @__PURE__ */ jsx(Tab, {
|
|
15632
|
-
className: !view.tabComponent
|
|
17183
|
+
className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
|
|
15633
17184
|
value: view.key,
|
|
15634
17185
|
children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
|
|
15635
17186
|
className: "flex items-center gap-1.5",
|
|
@@ -15640,7 +17191,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15640
17191
|
const customViewTabsEnd = resolvedEntityViews.filter((view) => !view.position || view.position === "end").map((view) => {
|
|
15641
17192
|
const icon = getIcon(view.icon, void 0, void 0, "smallest");
|
|
15642
17193
|
return /* @__PURE__ */ jsx(Tab, {
|
|
15643
|
-
className: !view.tabComponent
|
|
17194
|
+
className: cls(ENTITY_VIEW_TAB_CLASS, !view.tabComponent && "text-sm min-w-[90px]"),
|
|
15644
17195
|
value: view.key,
|
|
15645
17196
|
children: view.tabComponent ?? /* @__PURE__ */ jsxs("span", {
|
|
15646
17197
|
className: "flex items-center gap-1.5",
|
|
@@ -15653,6 +17204,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15653
17204
|
children: /* @__PURE__ */ jsx(IconButton, {
|
|
15654
17205
|
size: "small",
|
|
15655
17206
|
onClick: () => {
|
|
17207
|
+
carryEdit();
|
|
15656
17208
|
const editSuffix = collection.defaultEntityAction === "view" ? "/edit" : "";
|
|
15657
17209
|
const entityUrl = urlController.buildUrlCollectionPath(`${path}/${entityId}${editSuffix}`);
|
|
15658
17210
|
navigate(`${entityUrl}#full`);
|
|
@@ -15665,7 +17217,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15665
17217
|
children: [
|
|
15666
17218
|
/* @__PURE__ */ jsx(EntityIdentityBar, {
|
|
15667
17219
|
collection,
|
|
15668
|
-
collectionUrl: layout === "full_screen" || layout === "split" ?
|
|
17220
|
+
collectionUrl: layout === "full_screen" || layout === "split" ? withListState(urlController.buildUrlCollectionPath(path)) : void 0,
|
|
15669
17221
|
title: displayTitle,
|
|
15670
17222
|
entityId: status === "existing" ? entityId : void 0,
|
|
15671
17223
|
status,
|
|
@@ -15673,15 +17225,25 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15673
17225
|
saving: Boolean(formContext?.isSaving),
|
|
15674
17226
|
hasErrors: hasFormErrors,
|
|
15675
17227
|
saveDisabled: !canEdit || saveDisabled,
|
|
15676
|
-
onBack: layout === "full_screen" && !onShowList ? () => navigate(
|
|
17228
|
+
onBack: layout === "full_screen" && !onShowList ? () => navigate(withListState(urlController.buildUrlCollectionPath(path))) : void 0,
|
|
15677
17229
|
onSave: canEdit && formContext ? () => {
|
|
15678
17230
|
sideDialogContext.setPendingClose?.(false);
|
|
17231
|
+
pendingCloseRef.current = false;
|
|
15679
17232
|
formContext.submit();
|
|
15680
17233
|
} : void 0,
|
|
15681
17234
|
onSaveAndClose: canEdit && formContext && canCloseAfterSave ? () => {
|
|
17235
|
+
if (layout === "split") {
|
|
17236
|
+
pendingCloseRef.current = true;
|
|
17237
|
+
Promise.resolve(formContext.submit()).finally(() => {
|
|
17238
|
+
pendingCloseRef.current = false;
|
|
17239
|
+
});
|
|
17240
|
+
return;
|
|
17241
|
+
}
|
|
15682
17242
|
sideDialogContext.setPendingClose?.(true);
|
|
15683
17243
|
Promise.resolve(formContext.submit()).finally(() => sideDialogContext.setPendingClose?.(false));
|
|
15684
17244
|
} : void 0,
|
|
17245
|
+
saveAndClosePlacement: layout === "split" ? "menu" : "button",
|
|
17246
|
+
onClose: onCloseRequest,
|
|
15685
17247
|
onDiscard: canEdit && formContext ? () => formContext.formex.resetForm() : void 0,
|
|
15686
17248
|
onInspect: includeJsonView ? () => setInspectorTab("json") : void 0,
|
|
15687
17249
|
onViewHistory: includeHistoryView ? () => setInspectorTab("history") : void 0,
|
|
@@ -15690,8 +17252,11 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15690
17252
|
pluginActions: formPluginActions,
|
|
15691
17253
|
leading: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15692
17254
|
barActionsStart,
|
|
15693
|
-
layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, {}),
|
|
15694
|
-
layout === "full_screen" && onShowList && /* @__PURE__ */ jsx(SplitListShowButton, { onClick:
|
|
17255
|
+
layout === "split" && /* @__PURE__ */ jsx(SplitListCloseButton, { onBeforeHide: carryEdit }),
|
|
17256
|
+
layout === "full_screen" && onShowList && /* @__PURE__ */ jsx(SplitListShowButton, { onClick: () => {
|
|
17257
|
+
carryEdit();
|
|
17258
|
+
onShowList();
|
|
17259
|
+
} })
|
|
15695
17260
|
] }),
|
|
15696
17261
|
trailing: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15697
17262
|
pluginActionsTop,
|
|
@@ -15701,14 +17266,15 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15701
17266
|
entityId,
|
|
15702
17267
|
values: formContext?.values ?? usedEntity?.values ?? {},
|
|
15703
17268
|
status,
|
|
15704
|
-
dirty: Boolean(formContext?.formex?.dirty)
|
|
17269
|
+
dirty: Boolean(formContext?.formex?.dirty),
|
|
17270
|
+
carryEdit
|
|
15705
17271
|
})
|
|
15706
17272
|
] })
|
|
15707
17273
|
}),
|
|
15708
17274
|
hasAdditionalViews && /* @__PURE__ */ jsx("div", {
|
|
15709
|
-
className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", defaultBorderMixin),
|
|
17275
|
+
className: cls("h-10 shrink-0 flex items-stretch border-b px-2 min-w-0", "bg-surface-50 dark:bg-surface-900", defaultBorderMixin),
|
|
15710
17276
|
children: /* @__PURE__ */ jsxs(Tabs, {
|
|
15711
|
-
className: "!w-full",
|
|
17277
|
+
className: "!w-full items-stretch",
|
|
15712
17278
|
innerClassName: "h-full",
|
|
15713
17279
|
variant: "boxy",
|
|
15714
17280
|
value: activeTab,
|
|
@@ -15718,6 +17284,7 @@ function EditViewBindingInner({ path, entityId, selectedTab: selectedTabProp, co
|
|
|
15718
17284
|
children: [
|
|
15719
17285
|
/* @__PURE__ */ jsx(Tab, {
|
|
15720
17286
|
value: "__main_##Q$SC^#S6",
|
|
17287
|
+
className: cls("h-full data-[state=active]:bg-white dark:data-[state=active]:bg-surface-800", "text-sm min-w-[90px]"),
|
|
15721
17288
|
children: /* @__PURE__ */ jsxs("span", {
|
|
15722
17289
|
className: "flex items-center gap-1.5",
|
|
15723
17290
|
children: [getIcon(collection.icon, void 0, void 0, "smallest"), collection.singularName ?? collection.name]
|
|
@@ -15880,10 +17447,28 @@ function SidePanelBinding(props) {
|
|
|
15880
17447
|
setBlocked(false);
|
|
15881
17448
|
close(true);
|
|
15882
17449
|
};
|
|
17450
|
+
/**
|
|
17451
|
+
* The panel's response to a save, and the one place it decides where the
|
|
17452
|
+
* panel goes next.
|
|
17453
|
+
*
|
|
17454
|
+
* Exactly **one** panel navigation is raised here. These branches used to
|
|
17455
|
+
* run independently — a `replace` onto the saved record's address *and* a
|
|
17456
|
+
* close — and because the last navigation is the one that survives, which
|
|
17457
|
+
* of them the user got was settled by statement order rather than by
|
|
17458
|
+
* intent. Closing wins by construction now: moving a panel to a new address
|
|
17459
|
+
* it is about to leave is work whose only effect is to fight the close.
|
|
17460
|
+
*
|
|
17461
|
+
* `props.onUpdate` runs last for the same reason. It belongs to whoever
|
|
17462
|
+
* opened the panel and it is free to navigate — the reference picker's
|
|
17463
|
+
* selects the record and closes the picker — so the opener's intent should
|
|
17464
|
+
* be the final word, not something this panel overwrites on its way out.
|
|
17465
|
+
*/
|
|
15883
17466
|
const onUpdate = (params) => {
|
|
15884
|
-
if (props.onUpdate) props.onUpdate(params);
|
|
15885
17467
|
setEditInDialog(false);
|
|
15886
|
-
if (
|
|
17468
|
+
if (sideDialogsController.pendingClose || props.closeOnSave) {
|
|
17469
|
+
sideDialogsController.setPendingClose(false);
|
|
17470
|
+
closeAfterSave();
|
|
17471
|
+
} else if (params.status !== "existing") sidePanelController.replace({
|
|
15887
17472
|
path: params.path,
|
|
15888
17473
|
entityId: params.entityId,
|
|
15889
17474
|
selectedTab: params.selectedTab,
|
|
@@ -15891,10 +17476,7 @@ function SidePanelBinding(props) {
|
|
|
15891
17476
|
collection: params.collection
|
|
15892
17477
|
});
|
|
15893
17478
|
else closeEditView();
|
|
15894
|
-
|
|
15895
|
-
sideDialogsController.setPendingClose(false);
|
|
15896
|
-
closeAfterSave();
|
|
15897
|
-
}
|
|
17479
|
+
props.onUpdate?.(params);
|
|
15898
17480
|
};
|
|
15899
17481
|
const parentCollectionSlugs = useMemo(() => {
|
|
15900
17482
|
return collectionRegistryController.getParentCollectionSlugs(path);
|
|
@@ -16060,11 +17642,11 @@ function SidePanelBinding(props) {
|
|
|
16060
17642
|
onSaved: onUpdate,
|
|
16061
17643
|
navigateBack: closeEditView,
|
|
16062
17644
|
barActionsStart,
|
|
16063
|
-
barActions: ({
|
|
17645
|
+
barActions: ({ carryEdit }) => windowControls(allowFullScreen && /* @__PURE__ */ jsx(IconButton, {
|
|
16064
17646
|
className: "self-center",
|
|
16065
17647
|
size: "small",
|
|
16066
17648
|
onClick: () => {
|
|
16067
|
-
|
|
17649
|
+
carryEdit?.();
|
|
16068
17650
|
setBlocked(false);
|
|
16069
17651
|
setBlockedNavigationMessage(void 0);
|
|
16070
17652
|
if (entityId) {
|
|
@@ -17695,14 +19277,15 @@ function SelectionTableBindingInternal({ onSingleEntitySelected, onMultipleEntit
|
|
|
17695
19277
|
function EntitySelectionDialogActions({ collection, path, onClear, onNewClick }) {
|
|
17696
19278
|
const { canCreate } = usePermissions();
|
|
17697
19279
|
const largeLayout = useLargeLayout();
|
|
19280
|
+
const { t } = useTranslation();
|
|
17698
19281
|
const onClick = onNewClick ? (e) => {
|
|
17699
19282
|
e.preventDefault();
|
|
17700
19283
|
onNewClick();
|
|
17701
19284
|
} : void 0;
|
|
17702
|
-
const addButton = canCreate(collection, path) && onClick && (largeLayout ? /* @__PURE__ */
|
|
19285
|
+
const addButton = canCreate(collection, path) && onClick && (largeLayout ? /* @__PURE__ */ jsx(Button, {
|
|
17703
19286
|
onClick,
|
|
17704
19287
|
startIcon: /* @__PURE__ */ jsx(PlusIcon, {}),
|
|
17705
|
-
children:
|
|
19288
|
+
children: t("add_specific", { name: collection.singularName ?? collection.name })
|
|
17706
19289
|
}) : /* @__PURE__ */ jsx(Button, {
|
|
17707
19290
|
onClick,
|
|
17708
19291
|
children: /* @__PURE__ */ jsx(PlusIcon, {})
|
|
@@ -17924,8 +19507,8 @@ function BlockFieldBinding({ propertyKey, value, error, showError, isSubmitting,
|
|
|
17924
19507
|
addLabel: property.name ? t("add_to_field", { fieldName: property.name }) : t("add_entry"),
|
|
17925
19508
|
buildEntry,
|
|
17926
19509
|
onInternalIdAdded: setLastAddedId,
|
|
17927
|
-
disabled: isSubmitting ||
|
|
17928
|
-
canAddElements: !property
|
|
19510
|
+
disabled: isSubmitting || isDisabled(property),
|
|
19511
|
+
canAddElements: !isDisabled(property),
|
|
17929
19512
|
onValueChange: (value) => setFieldValue(propertyKey, value),
|
|
17930
19513
|
newDefaultEntry: {
|
|
17931
19514
|
[property.oneOf.typeField ?? DEFAULT_ONE_OF_TYPE]: firstOneOfKey,
|
|
@@ -18061,7 +19644,8 @@ function DateTimeFieldBinding({ propertyKey, value, setValue, autoFocus, error,
|
|
|
18061
19644
|
required: property.validation?.required,
|
|
18062
19645
|
className: showError ? "text-red-500 dark:text-red-500" : "text-text-secondary dark:text-text-secondary-dark",
|
|
18063
19646
|
title: property.name ?? propertyKey
|
|
18064
|
-
})
|
|
19647
|
+
}),
|
|
19648
|
+
"aria-label": hideLabel ? property.name ?? propertyKey : void 0
|
|
18065
19649
|
})
|
|
18066
19650
|
}), /* @__PURE__ */ jsx(FieldHelperText, {
|
|
18067
19651
|
includeDescription,
|
|
@@ -18218,6 +19802,7 @@ function MapKeyValueRow({ id, fieldKey, value, onFieldKeyChange, onDeleteClick,
|
|
|
18218
19802
|
const { t } = useTranslation();
|
|
18219
19803
|
function buildInput(entryValue, fieldKey, type) {
|
|
18220
19804
|
if (type === "string" || type === "number") return /* @__PURE__ */ jsx(TextField, {
|
|
19805
|
+
"aria-label": fieldKey ? `${t("value")}: ${fieldKey}` : t("value"),
|
|
18221
19806
|
placeholder: "value",
|
|
18222
19807
|
value: entryValue,
|
|
18223
19808
|
type: type === "number" ? "number" : "text",
|
|
@@ -18333,6 +19918,7 @@ function MapKeyValueRow({ id, fieldKey, value, onFieldKeyChange, onDeleteClick,
|
|
|
18333
19918
|
className: "w-[300px] max-w-[30%]",
|
|
18334
19919
|
children: /* @__PURE__ */ jsx(TextField, {
|
|
18335
19920
|
value: fieldKey,
|
|
19921
|
+
"aria-label": t("key"),
|
|
18336
19922
|
placeholder: "key",
|
|
18337
19923
|
disabled: disabled || entryValue !== void 0 && entryValue !== null && entryValue !== "",
|
|
18338
19924
|
size: "medium",
|
|
@@ -18403,6 +19989,7 @@ function ArrayKeyValueRow({ id, index, value, setValue }) {
|
|
|
18403
19989
|
function buildInput(entryValue, type) {
|
|
18404
19990
|
if (type === "string" || type === "number") return /* @__PURE__ */ jsx(TextField, {
|
|
18405
19991
|
value: entryValue,
|
|
19992
|
+
"aria-label": `Value ${index + 1}`,
|
|
18406
19993
|
type: type === "number" ? "number" : "text",
|
|
18407
19994
|
size: "medium",
|
|
18408
19995
|
onChange: (event) => {
|
|
@@ -18523,7 +20110,7 @@ function MapFieldBinding({ propertyKey, value, showError, error, disabled, prope
|
|
|
18523
20110
|
const mapFormView = /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
|
|
18524
20111
|
className: "py-1 flex flex-col space-y-2",
|
|
18525
20112
|
children: Object.entries(mapProperties).filter(([_, property]) => !isHidden(property)).map(([entryKey, childProperty], index) => {
|
|
18526
|
-
const thisDisabled = isReadOnly(childProperty) ||
|
|
20113
|
+
const thisDisabled = isReadOnly(childProperty) || isDisabled(childProperty);
|
|
18527
20114
|
return /* @__PURE__ */ jsx("div", {
|
|
18528
20115
|
className: "relative",
|
|
18529
20116
|
children: /* @__PURE__ */ jsx(ErrorBoundary, { children: /* @__PURE__ */ jsx(PropertyFieldBinding, {
|
|
@@ -18960,7 +20547,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
|
|
|
18960
20547
|
index
|
|
18961
20548
|
}) });
|
|
18962
20549
|
};
|
|
18963
|
-
const canAddElements = !property
|
|
20550
|
+
const canAddElements = !isDisabled(property) && !isSubmitting && !disabled && (property.admin?.canAddElements || property.admin?.canAddElements === void 0);
|
|
18964
20551
|
const sortable = property.admin?.sortable === void 0 ? true : property.admin.sortable;
|
|
18965
20552
|
const arrayContainer = /* @__PURE__ */ jsx(ArrayContainer, {
|
|
18966
20553
|
droppableId: propertyKey,
|
|
@@ -18968,7 +20555,7 @@ function RepeatFieldBinding({ propertyKey, value, error, showError, isSubmitting
|
|
|
18968
20555
|
value: value ?? [],
|
|
18969
20556
|
buildEntry,
|
|
18970
20557
|
onInternalIdAdded: setLastAddedId,
|
|
18971
|
-
disabled: isSubmitting ||
|
|
20558
|
+
disabled: isSubmitting || isDisabled(property),
|
|
18972
20559
|
canAddElements,
|
|
18973
20560
|
sortable,
|
|
18974
20561
|
newDefaultEntry: getDefaultValueFor(property.of),
|
|
@@ -19125,7 +20712,7 @@ var rejectDropClasses = "transition-colors duration-200 ease-[cubic-bezier(0,0,0
|
|
|
19125
20712
|
function StorageUploadFieldBinding({ propertyKey, value, setValue, error, showError, autoFocus, minimalistView, property, includeDescription, hideLabel, context, isSubmitting }) {
|
|
19126
20713
|
useAuthController();
|
|
19127
20714
|
const storageSource = useStorageSource();
|
|
19128
|
-
const disabled = isReadOnly(property) ||
|
|
20715
|
+
const disabled = isReadOnly(property) || isDisabled(property) || isSubmitting || context.disabled;
|
|
19129
20716
|
const { internalValue, setInternalValue, onFilesAdded, storage, onFileUploadComplete, storagePathBuilder, multipleFilesSupported, resolvedStorageSource } = useStorageUploadController({
|
|
19130
20717
|
entityValues: context.values,
|
|
19131
20718
|
entityId: context.entityId,
|
|
@@ -19452,6 +21039,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
|
|
|
19452
21039
|
required: property.validation?.required || property.isId === true,
|
|
19453
21040
|
title: property.name ?? propertyKey
|
|
19454
21041
|
});
|
|
21042
|
+
const accessibleName = property.name ?? propertyKey;
|
|
19455
21043
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
19456
21044
|
/* @__PURE__ */ jsx(PropertyIdCopyTooltip, {
|
|
19457
21045
|
propertyKey,
|
|
@@ -19467,6 +21055,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
|
|
|
19467
21055
|
onChange,
|
|
19468
21056
|
autoFocus,
|
|
19469
21057
|
disabled,
|
|
21058
|
+
"aria-label": accessibleName,
|
|
19470
21059
|
className: cls("rounded-md resize-none w-full outline-none text-base bg-transparent min-h-[64px] px-3", label ? "pt-8 pb-2" : "py-2", disabled && "outline-none opacity-50 text-surface-accent-600 dark:text-surface-accent-500", showError && error ? "text-red-500 dark:text-red-600" : "")
|
|
19471
21060
|
}),
|
|
19472
21061
|
property.admin?.clearable && /* @__PURE__ */ jsx("div", {
|
|
@@ -19483,6 +21072,7 @@ function TextFieldBinding({ propertyKey, value, setValue, error, showError, disa
|
|
|
19483
21072
|
onChange,
|
|
19484
21073
|
autoFocus,
|
|
19485
21074
|
label,
|
|
21075
|
+
"aria-label": label ? void 0 : accessibleName,
|
|
19486
21076
|
type: inputType,
|
|
19487
21077
|
disabled,
|
|
19488
21078
|
endAdornment: property.admin?.clearable && /* @__PURE__ */ jsx(IconButton, {
|
|
@@ -19618,6 +21208,7 @@ function VectorFieldBinding({ propertyKey, value, setValue, error, showError, di
|
|
|
19618
21208
|
className: cls("flex flex-col gap-2 p-4 rounded-xl border bg-surface-50/20 dark:bg-surface-800/10", defaultBorderMixin),
|
|
19619
21209
|
children: [/* @__PURE__ */ jsx(TextField, {
|
|
19620
21210
|
size,
|
|
21211
|
+
"aria-label": `${property.name ?? propertyKey} vector values`,
|
|
19621
21212
|
value: textValue,
|
|
19622
21213
|
onChange,
|
|
19623
21214
|
autoFocus: true,
|
|
@@ -19901,6 +21492,7 @@ function BinaryFieldBinding({ propertyKey, value, setValue, error, showError, di
|
|
|
19901
21492
|
className: cls("flex flex-col gap-2 p-4 rounded-xl border", defaultBorderMixin),
|
|
19902
21493
|
children: [/* @__PURE__ */ jsx(TextField, {
|
|
19903
21494
|
size,
|
|
21495
|
+
"aria-label": `${property.name ?? propertyKey} base64 value`,
|
|
19904
21496
|
value: text,
|
|
19905
21497
|
onChange,
|
|
19906
21498
|
autoFocus: autoFocus ?? true,
|
|
@@ -20698,7 +22290,7 @@ function PropertyFieldBindingInternal({ propertyKey, property, context, includeD
|
|
|
20698
22290
|
authController
|
|
20699
22291
|
});
|
|
20700
22292
|
const readOnly = resolvedProperty ? isReadOnly(resolvedProperty) : true;
|
|
20701
|
-
const disabled = disabledProp || readOnly ||
|
|
22293
|
+
const disabled = disabledProp || readOnly || (resolvedProperty ? isDisabled(resolvedProperty) : false) || context.disabled;
|
|
20702
22294
|
if (resolvedProperty === null || isHidden(resolvedProperty)) return /* @__PURE__ */ jsx(Fragment, {});
|
|
20703
22295
|
else if (readOnly) Component = ReadOnlyFieldBinding;
|
|
20704
22296
|
else if (resolvedProperty.admin?.Field) {
|
|
@@ -21277,96 +22869,6 @@ function LocalChangesMenu({ cachedData, formex, onClearLocalChanges, cacheKey, p
|
|
|
21277
22869
|
})] });
|
|
21278
22870
|
}
|
|
21279
22871
|
//#endregion
|
|
21280
|
-
//#region src/form/form_utils.ts
|
|
21281
|
-
function extractTouchedValues(values, touched) {
|
|
21282
|
-
let acc = {};
|
|
21283
|
-
if (!touched || typeof touched !== "object") return acc;
|
|
21284
|
-
Object.entries(touched).forEach(([key, value]) => {
|
|
21285
|
-
if (value) acc = setIn(acc, key, getIn(values, key));
|
|
21286
|
-
});
|
|
21287
|
-
return acc;
|
|
21288
|
-
}
|
|
21289
|
-
/**
|
|
21290
|
-
* Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
|
|
21291
|
-
* This prevents ghost containers created by `setIn` intermediate path construction
|
|
21292
|
-
* (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
|
|
21293
|
-
* from falsely triggering the unsaved local changes indicator.
|
|
21294
|
-
*/
|
|
21295
|
-
/**
|
|
21296
|
-
* Check if a value is semantically empty (null, undefined, or empty string).
|
|
21297
|
-
*/
|
|
21298
|
-
function isSemanticEmpty(v) {
|
|
21299
|
-
return v === null || v === void 0 || v === "";
|
|
21300
|
-
}
|
|
21301
|
-
function removeEmptyContainers(obj) {
|
|
21302
|
-
if (Array.isArray(obj)) return obj.map(removeEmptyContainers);
|
|
21303
|
-
if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
|
|
21304
|
-
const result = {};
|
|
21305
|
-
for (const key of Object.keys(obj)) {
|
|
21306
|
-
const cleaned = removeEmptyContainers(obj[key]);
|
|
21307
|
-
if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned) && Object.getPrototypeOf(cleaned) === Object.prototype && Object.keys(cleaned).length === 0) continue;
|
|
21308
|
-
result[key] = cleaned;
|
|
21309
|
-
}
|
|
21310
|
-
if (Object.keys(result).length > 0 && Object.values(result).every(isSemanticEmpty)) return {};
|
|
21311
|
-
return result;
|
|
21312
|
-
}
|
|
21313
|
-
return obj;
|
|
21314
|
-
}
|
|
21315
|
-
function getChanges(source, comparison) {
|
|
21316
|
-
const changes = {};
|
|
21317
|
-
if (!source) return {};
|
|
21318
|
-
if (!comparison) return source;
|
|
21319
|
-
const allKeys = Array.from(/* @__PURE__ */ new Set([...Object.keys(source), ...Object.keys(comparison)]));
|
|
21320
|
-
for (const key of allKeys) {
|
|
21321
|
-
const sourceValue = source[key];
|
|
21322
|
-
const comparisonValue = comparison[key];
|
|
21323
|
-
if (deepEqual(sourceValue, comparisonValue)) continue;
|
|
21324
|
-
const sourceHasKey = source && typeof source === "object" && Object.prototype.hasOwnProperty.call(source, key);
|
|
21325
|
-
if (comparison && typeof comparison === "object" && Object.prototype.hasOwnProperty.call(comparison, key) && !sourceHasKey) changes[key] = void 0;
|
|
21326
|
-
else if (Array.isArray(sourceValue)) {
|
|
21327
|
-
const comparisonArray = Array.isArray(comparisonValue) ? comparisonValue : [];
|
|
21328
|
-
if (sourceValue.length !== comparisonArray.length) {
|
|
21329
|
-
changes[key] = sourceValue;
|
|
21330
|
-
continue;
|
|
21331
|
-
}
|
|
21332
|
-
if (sourceValue.some((item, index) => !deepEqual(item, comparisonArray[index]))) changes[key] = sourceValue;
|
|
21333
|
-
} else if (isObject(sourceValue) && sourceValue && isObject(comparisonValue) && comparisonValue) {
|
|
21334
|
-
const nestedChanges = getChanges(sourceValue, comparisonValue);
|
|
21335
|
-
if (Object.keys(nestedChanges).length > 0) changes[key] = nestedChanges;
|
|
21336
|
-
} else changes[key] = sourceValue;
|
|
21337
|
-
}
|
|
21338
|
-
return changes;
|
|
21339
|
-
}
|
|
21340
|
-
function getInitialEntityValues(authController, collection, path, status, entity, propertyConfigs) {
|
|
21341
|
-
const properties = collection.properties;
|
|
21342
|
-
if ((status === "existing" || status === "copy") && entity) {
|
|
21343
|
-
let values;
|
|
21344
|
-
if (!collection.alwaysApplyDefaultValues) values = entity.values ?? getDefaultValuesFor(properties);
|
|
21345
|
-
else values = mergeDeep(getDefaultValuesFor(properties), entity.values ?? {});
|
|
21346
|
-
if (status === "copy") {
|
|
21347
|
-
const result = { ...values };
|
|
21348
|
-
for (const [key, property] of Object.entries(properties)) if (property && "isId" in property && property.isId) delete result[key];
|
|
21349
|
-
return result;
|
|
21350
|
-
}
|
|
21351
|
-
return values;
|
|
21352
|
-
} else if (status === "new") return getDefaultValuesFor(properties);
|
|
21353
|
-
else {
|
|
21354
|
-
console.error({
|
|
21355
|
-
status,
|
|
21356
|
-
entity
|
|
21357
|
-
});
|
|
21358
|
-
throw new Error("Form has not been initialised with the correct parameters");
|
|
21359
|
-
}
|
|
21360
|
-
}
|
|
21361
|
-
function zodToFormErrors(zodError) {
|
|
21362
|
-
let errors = {};
|
|
21363
|
-
for (const issue of zodError.issues) {
|
|
21364
|
-
const path = issue.path.join(".");
|
|
21365
|
-
if (path && !getIn(errors, path)) errors = setIn(errors, path, issue.message);
|
|
21366
|
-
}
|
|
21367
|
-
return errors;
|
|
21368
|
-
}
|
|
21369
|
-
//#endregion
|
|
21370
22872
|
//#region src/form/EntityForm.tsx
|
|
21371
22873
|
/**
|
|
21372
22874
|
* Headless entity form component.
|
|
@@ -21623,7 +23125,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
|
|
|
21623
23125
|
function isFieldDisabled(property) {
|
|
21624
23126
|
const isNew = status === "new" || status === "copy";
|
|
21625
23127
|
const isIdAndAuto = (property.type === "string" || property.type === "number") && "isId" in property && typeof property.isId === "string" && property.isId !== "manual";
|
|
21626
|
-
return Boolean(disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) ||
|
|
23128
|
+
return Boolean(disabledProp || !autoSave && formex.isSubmitting || isReadOnly(property) || isDisabled(property) || !isNew && "isId" in property && Boolean(property.isId) || isNew && isIdAndAuto);
|
|
21627
23129
|
}
|
|
21628
23130
|
const renderField = (field) => {
|
|
21629
23131
|
if (field.additional) {
|
|
@@ -21798,7 +23300,7 @@ function EntityForm({ path, entityId: entityIdProp, collection, onValuesModified
|
|
|
21798
23300
|
className: "flex-1 min-w-0 overflow-y-auto flex justify-center items-start",
|
|
21799
23301
|
children: /* @__PURE__ */ jsxs("div", {
|
|
21800
23302
|
id: `form_${path}`,
|
|
21801
|
-
className: cls("@container/col w-full max-w-3xl 2xl:max-w-4xl flex flex-col", openEntityMode === "dialog" ? "pt-5 pb-8 px-6 sm:px-8" : "pt-6 pb-12 px-5 sm:px-8"),
|
|
23303
|
+
className: cls("@container/col w-full max-w-3xl @7xl/form:max-w-4xl 2xl:max-w-4xl flex flex-col", openEntityMode === "dialog" ? "pt-5 pb-8 px-6 sm:px-8" : "pt-6 pb-12 px-5 sm:px-8"),
|
|
21802
23304
|
children: [manualApplyLocalChanges && hasLocalChanges && localChangesCacheKey && /* @__PURE__ */ jsx("div", {
|
|
21803
23305
|
className: "flex justify-end mb-3",
|
|
21804
23306
|
children: /* @__PURE__ */ jsx(LocalChangesMenu, {
|
|
@@ -21886,9 +23388,7 @@ function EntityFormBinding({ path, entityId: entityIdProp, collection, entity, i
|
|
|
21886
23388
|
const localChangesData = useMemo(() => {
|
|
21887
23389
|
if (!localChangesDataRaw) return void 0;
|
|
21888
23390
|
const openingValues = initialDirtyValues ? mergeDeep(computedInitialValues, initialDirtyValues) : computedInitialValues;
|
|
21889
|
-
|
|
21890
|
-
if (cleaned && typeof cleaned === "object" && Object.keys(cleaned).length === 0) return;
|
|
21891
|
-
return cleaned;
|
|
23391
|
+
return getUnappliedLocalChanges(localChangesDataRaw, openingValues);
|
|
21892
23392
|
}, [
|
|
21893
23393
|
localChangesDataRaw,
|
|
21894
23394
|
computedInitialValues,
|
|
@@ -22939,6 +24439,6 @@ function getFullIdPath(propertyKey, propertyNamespace) {
|
|
|
22939
24439
|
return idToPropertiesPath(propertyNamespace ? `${propertyNamespace}.${propertyKey}` : propertyKey);
|
|
22940
24440
|
}
|
|
22941
24441
|
//#endregion
|
|
22942
|
-
export {
|
|
24442
|
+
export { getEntityViewWidth as $, SkeletonPropertyComponent as $n, resolveCollectionPathIds$1 as $t, SelectFieldBinding as A, ArrayOfStringsPreview as An, saveImportedEntities as At, useSelectionDialog as B, useSidePanel as Bn, FieldCaption as Bt, getDefaultFieldId as C, UserPreview as Cn, isReferenceProperty as Cr, convertDataToEntity as Ct, TextFieldBinding as D, KeyValuePreview as Dn, useImportConfig as Dt, VectorFieldBinding as E, DatePreview as En, getInferenceType as Et, MapFieldBinding as F, ArrayOfReferencesPreview as Fn, parseCsvToObjects as Ft, useTopLevelNavigation as G, getEntityPreviewKeys as Gn, mergeEntityActions as Gt, SideDialogs as H, useCollectionRegistryController as Hn, BreadcrumbsProvider as Ht, KeyValueFieldBinding as I, InlineEntityListPreview as In, unflattenObject as It, useBuildCollectionRegistryController as J, ArrayPropertyPreview as Jn, getCollectionPathsCombinations as Jt, useResolvedViews as K, getEntityTitlePropertyKey as Kn, addInitialSlash as Kt, DateTimeFieldBinding as L, ReferencePreview as Ln, ArrayContainer as Lt, ReferenceFieldBinding as M, ArrayEnumPreview as Mn, convertFileToJson as Mt, MultiSelectFieldBinding as N, ArrayOfStorageComponentsPreview as Nn, detectCsvDelimiter as Nt, SwitchFieldBinding as O, MapPropertyPreview as On, ImportSaveInProgress as Ot, MarkdownEditorFieldBinding as P, RelationPreview as Pn, parseCsvRows as Pt, buildSidePanelsFromUrl as Q, StorageThumbnailInternal as Qn, removeTrailingSlash$1 as Qt, BlockFieldBinding as R, EntityPreviewBinding as Rn, PropertyConfigBadge as Rt, getDefaultFieldConfig as S, PropertyPreview as Sn, getResolvedPropertyInPath as Sr, DataNewPropertiesMapping as St, getFieldId as T, BooleanPreview as Tn, processValueMapping as Tt, useBuildUrlController as U, getUserLabel as Un, resolveEntityAction as Ut, SelectionTableBinding as V, CollectionRegistryContext as Vn, useBreadcrumbsController as Vt, useBuildNavigationStateController as W, useResolvedUser as Wn, resolveEntityView as Wt, resolveNavigationFrom as X, EnumValuesChip as Xn, removeInitialAndTrailingSlashes$1 as Xt, useHistory as Y, StringPropertyPreview as Yn, getLastSegment$1 as Yt, useResolvedNavigationFrom as Z, StorageThumbnail as Zn, removeInitialSlash as Zt, useCollectionsConfigController as _, ReadOnlyFieldBinding as _n, getDefaultPropertiesOrder as _r, CollectionViewActions as _t, namespaceToPropertiesPath as a, SelectableTable as an, ImagePreview as ar, removeEmptyContainers as at, PropertyFieldBinding as b, FieldHelperText as bn, getPropertiesWithPropertiesOrder as br, useCollectionEditorDialogsState as bt, buildCollectionGenerationCallback as c, UrlContext as cn, FormSections as cr, copyEntityAction as ct, fromSerializableCollectionConfigs as d, useNavigationStateController as dn, FieldBlock as dr, resetPasswordAction as dt, resolveOpenEntityMode as en, renderSkeletonCaptionText as er, useBuildSidePanel as et, fromSerializableProperties as f, useSideDialogsController as fn, LABEL_ICON_SIZE as fr, CreationResultDialog as ft, toSerializableProperty as g, useClearRestoreValue as gn, getBracketNotation as gr, EntityCardBinding as gt, toSerializableProperties as h, ArrayCustomShapedFieldBinding as hn, PropertyIdCopyTooltip as hr, CollectionCardViewBinding as ht, namespaceToPropertiesOrderPath as i, CollectionTableBinding as in, UrlComponentPreview as ir, getInitialEntityValues as it, RepeatFieldBinding as j, ArrayPropertyEnumPreview as jn, ImportFileUpload as jt, StorageUploadFieldBinding as k, ArrayOneOfPreview as kn, IMPORT_BATCH_SIZE as kt, validateCollectionJson as l, useUrlController as ln, FormRail as lr, deleteEntityAction as lt, toSerializableCollectionConfig as m, SelectableTableContext as mn, spanClass as mr, EntityViewBinding as mt, getFullIdPath as n, useSelectionController as nn, renderSkeletonImageThumbnail as nr, extractTouchedValues as nt, CollectionGenerationApiError as o, CollectionRowActions as on, sanitizeUrl as or, zodToFormErrors as ot, fromSerializableProperty as p, SideDialogsControllerContext as pn, isSelfLabellingProperty as pr, DetailViewBinding as pt, useResolvedCollections as q, getEntityTitlePropertyKeyForEntity as qn, getCollectionBySlugWithin as qt, idToPropertiesPath as r, VirtualTableInput$1 as rn, renderSkeletonText as rr, getChanges as rt, DEFAULT_COLLECTION_GENERATION_ENDPOINT as s, useAdminContext as sn, EmptyValue as sr, CollectionViewBinding as st, getFullId as t, resolveViewMode as tn, renderSkeletonIcon as tr, EditViewBinding as tt, fromSerializableCollectionConfig as u, NavigationStateContext as un, RecordMeta as ur, editEntityAction as ut, EntityFormBinding as v, LabelWithIconAndTooltip as vn, getIconForProperty as vr, useCollectionEditorController as vt, getFieldConfig as w, NumberPropertyPreview as wn, isRelationProperty as wr, flattenEntry as wt, DEFAULT_FIELD_CONFIGS as x, ArrayOfMapsPreview as xn, getPropertyInPath$1 as xr, ImportNewPropertyFieldPreview as xt, EntityForm as y, LabelWithIcon as yn, getIconForWidget as yr, ConfigControllerProvider as yt, ArrayOfReferencesFieldBinding as z, SidePanelControllerContext as zn, SearchIconsView as zt };
|
|
22943
24443
|
|
|
22944
|
-
//# sourceMappingURL=util-
|
|
24444
|
+
//# sourceMappingURL=util-C-D5yD2n.js.map
|