@firecms/core 3.3.0-canary.3ea2cd2 → 3.3.0-canary.5906216

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.
Files changed (70) hide show
  1. package/dist/app/useApp.d.ts +1 -0
  2. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  3. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  4. package/dist/components/EntityPreview.d.ts +3 -1
  5. package/dist/core/DefaultDrawer.d.ts +10 -3
  6. package/dist/core/field_configs.d.ts +1 -1
  7. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  8. package/dist/form/index.d.ts +1 -0
  9. package/dist/index.es.js +2090 -743
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/index.umd.js +2089 -742
  12. package/dist/index.umd.js.map +1 -1
  13. package/dist/locales/pl.d.ts +2 -0
  14. package/dist/preview/index.d.ts +1 -0
  15. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  16. package/dist/types/collections.d.ts +17 -0
  17. package/dist/types/translations.d.ts +9 -1
  18. package/dist/util/entities.d.ts +1 -0
  19. package/dist/util/geopoint.d.ts +22 -0
  20. package/dist/util/index.d.ts +1 -0
  21. package/dist/util/navigation_blocking.d.ts +22 -0
  22. package/dist/util/navigation_from_path.d.ts +10 -0
  23. package/dist/util/navigation_utils.d.ts +20 -0
  24. package/package.json +16 -9
  25. package/src/app/Scaffold.tsx +47 -45
  26. package/src/app/useApp.tsx +1 -0
  27. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +2 -1
  28. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  29. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  30. package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -2
  31. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
  32. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  33. package/src/components/EntityPreview.tsx +41 -40
  34. package/src/components/ReferenceWidget.tsx +1 -1
  35. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  36. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  37. package/src/components/common/useDataSourceTableController.tsx +66 -10
  38. package/src/core/DefaultDrawer.tsx +150 -64
  39. package/src/core/DrawerNavigationGroup.tsx +27 -29
  40. package/src/core/DrawerNavigationItem.tsx +10 -12
  41. package/src/core/EntityEditView.tsx +57 -32
  42. package/src/core/field_configs.tsx +15 -0
  43. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  44. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  45. package/src/form/index.tsx +1 -0
  46. package/src/hooks/useBuildNavigationController.tsx +5 -1
  47. package/src/i18n/FireCMSi18nProvider.tsx +2 -0
  48. package/src/internal/useBuildSideEntityController.tsx +2 -1
  49. package/src/locales/de.ts +11 -3
  50. package/src/locales/en.ts +11 -3
  51. package/src/locales/es.ts +11 -3
  52. package/src/locales/fr.ts +11 -3
  53. package/src/locales/hi.ts +11 -3
  54. package/src/locales/it.ts +11 -3
  55. package/src/locales/pl.ts +730 -0
  56. package/src/locales/pt.ts +11 -3
  57. package/src/preview/PropertyPreview.tsx +12 -0
  58. package/src/preview/index.ts +1 -0
  59. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  60. package/src/routes/FireCMSRoute.tsx +44 -25
  61. package/src/types/collections.ts +18 -0
  62. package/src/types/translations.ts +9 -1
  63. package/src/util/entities.ts +13 -0
  64. package/src/util/geopoint.ts +81 -0
  65. package/src/util/index.ts +1 -0
  66. package/src/util/navigation_blocking.ts +45 -0
  67. package/src/util/navigation_from_path.ts +23 -6
  68. package/src/util/navigation_utils.ts +36 -2
  69. package/src/util/parent_references_from_path.ts +4 -2
  70. package/src/util/resolutions.ts +3 -0
@@ -10,7 +10,16 @@ import {
10
10
  import { Link, useNavigate } from "react-router-dom";
11
11
  import { CMSAnalyticsEvent, NavigationEntry, NavigationResult } from "../types";
12
12
  import { IconForView } from "../util";
13
- import { cls, IconButton, Menu, MenuItem, MoreVertIcon, Tooltip } from "@firecms/ui";
13
+ import {
14
+ cls,
15
+ KeyboardDoubleArrowLeftIcon,
16
+ KeyboardDoubleArrowRightIcon,
17
+ Menu,
18
+ MenuItem,
19
+ MoreVertIcon,
20
+ Tooltip,
21
+ Typography
22
+ } from "@firecms/ui";
14
23
  import { useAnalyticsController } from "../hooks/useAnalyticsController";
15
24
  import { DrawerNavigationGroup } from "./DrawerNavigationGroup";
16
25
  import { FireCMSLogo } from "../components";
@@ -37,6 +46,14 @@ export function DefaultDrawer({
37
46
 
38
47
  const [adminMenuOpen, setAdminMenuOpen] = React.useState(false);
39
48
 
49
+ const scrollRef = React.useRef<HTMLDivElement>(null);
50
+ const [scrolled, setScrolled] = React.useState(false);
51
+ const handleScroll = () => {
52
+ if (scrollRef.current) {
53
+ setScrolled(scrollRef.current.scrollTop > 0);
54
+ }
55
+ };
56
+
40
57
  const analyticsController = useAnalyticsController();
41
58
  const navigation = useNavigationController();
42
59
  const { t } = useTranslation();
@@ -59,6 +76,8 @@ export function DefaultDrawer({
59
76
  // Collapsible groups state - using "drawer" namespace for independent state from home page
60
77
  const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupsWithoutAdmin, "drawer");
61
78
 
79
+ const drawerVisuallyOpen = drawerOpen || drawerHovered;
80
+
62
81
  const onItemClick = (view: NavigationEntry) => {
63
82
  const eventName: CMSAnalyticsEvent = view.type === "collection"
64
83
  ? "drawer_navigate_to_collection"
@@ -69,56 +88,66 @@ export function DefaultDrawer({
69
88
  };
70
89
 
71
90
  return (
72
- <>
73
- <div className={cls("flex flex-col h-full relative flex-grow w-full", className)} style={style}>
74
-
75
- <DrawerLogo logo={logo} />
76
-
77
- <div className={"mt-4 flex-grow overflow-scroll no-scrollbar"}
78
- style={{
79
- maskImage: "linear-gradient(to bottom, transparent 0, black 20px, black calc(100% - 20px), transparent 100%)",
80
- }}>
81
-
82
- {groupsWithoutAdmin.map((group) => {
83
- const entriesInGroup = Object.values(navigationEntries).filter(e => e.group === group);
84
- return (
85
- <DrawerNavigationGroup
86
- key={`drawer_group_${group}`}
87
- group={group}
88
- entries={entriesInGroup}
89
- collapsed={isGroupCollapsed(group)}
90
- onToggleCollapsed={() => toggleGroupCollapsed(group)}
91
- drawerOpen={drawerOpen}
92
- tooltipsOpen={tooltipsOpen}
93
- adminMenuOpen={adminMenuOpen}
94
- onItemClick={onItemClick}
95
- />
96
- );
97
- })}
91
+ <div role="navigation" aria-label="Main navigation"
92
+ className={cls("flex flex-col h-full relative grow w-full", className)} style={style}>
98
93
 
99
- </div>
94
+ <DrawerLogo logo={logo} />
95
+
96
+ <div
97
+ ref={scrollRef}
98
+ onScroll={handleScroll}
99
+ className={"flex-grow min-h-0 overflow-y-auto overflow-x-hidden no-scrollbar px-2"}
100
+ style={{
101
+ maskImage: scrolled
102
+ ? "linear-gradient(to bottom, transparent 0, black 20px, black calc(100% - 20px), transparent 100%)"
103
+ : "linear-gradient(to bottom, black 0, black calc(100% - 20px), transparent 100%)"
104
+ }}>
105
+
106
+ {groupsWithoutAdmin.map((group) => {
107
+ const entriesInGroup = Object.values(navigationEntries).filter(e => e.group === group);
108
+ return (
109
+ <DrawerNavigationGroup
110
+ key={`drawer_group_${group}`}
111
+ group={group}
112
+ entries={entriesInGroup}
113
+ collapsed={isGroupCollapsed(group)}
114
+ onToggleCollapsed={() => toggleGroupCollapsed(group)}
115
+ drawerOpen={drawerVisuallyOpen}
116
+ tooltipsOpen={tooltipsOpen}
117
+ adminMenuOpen={adminMenuOpen}
118
+ onItemClick={onItemClick}
119
+ />
120
+ );
121
+ })}
100
122
 
101
- {adminViews.length > 0 && <Menu
123
+ </div>
124
+
125
+ {adminViews.length > 0 && <div className={"shrink-0 px-4"}>
126
+ <Menu
102
127
  side={"right"}
103
128
  open={adminMenuOpen}
104
129
  onOpenChange={setAdminMenuOpen}
105
130
  trigger={
106
- <IconButton
107
- shape={"square"}
108
- className={"m-4 text-surface-900 dark:text-white w-fit"}>
109
- <Tooltip title={"Admin"}
110
- open={tooltipsOpen}
111
- side={"right"} sideOffset={28}>
112
- <MoreVertIcon />
113
- </Tooltip>
114
- {drawerOpen && <div
131
+ <div
132
+ className={cls(
133
+ "flex flex-row items-center rounded-lg cursor-pointer w-full",
134
+ "hover:bg-surface-accent-100 dark:hover:bg-surface-800 transition-colors duration-150 h-[30px]"
135
+ )}>
136
+ <div
137
+ className={"shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-surface-400"}>
138
+ <Tooltip title={"Admin"}
139
+ open={drawerVisuallyOpen || adminMenuOpen ? false : tooltipsOpen}
140
+ side={"right"} sideOffset={28}>
141
+ <MoreVertIcon size={"small"} />
142
+ </Tooltip>
143
+ </div>
144
+ {drawerVisuallyOpen && <div
115
145
  className={cls(
116
- drawerOpen ? "opacity-100" : "opacity-0 hidden",
117
- "mx-4 font-inherit text-inherit"
146
+ "font-semibold text-[11px] uppercase tracking-wider text-surface-400"
118
147
  )}>
119
- ADMIN
148
+ {t("admin")}
120
149
  </div>}
121
- </IconButton>}
150
+ </div>}
122
151
  >
123
152
  {adminViews.map((entry) =>
124
153
  <MenuItem
@@ -131,46 +160,103 @@ export function DefaultDrawer({
131
160
  {t(entry.name)}
132
161
  </MenuItem>)}
133
162
 
134
- </Menu>}
135
- </div>
163
+ </Menu>
164
+ </div>}
136
165
 
137
- </>
166
+ <DrawerToggle />
167
+ </div>
138
168
  );
139
169
  }
140
170
 
141
171
  /**
142
- * This is the logo displayed in the drawer
143
- * It expands when the drawer is open.
172
+ * Collapse/expand toggle rendered at the bottom of the drawer.
173
+ * Uses double-chevron icons to indicate direction. Reads the drawer state
174
+ * from {@link useApp} so it can be dropped into any drawer.
144
175
  *
145
- * @param logo
176
+ * @group Core
177
+ */
178
+ export function DrawerToggle() {
179
+ const {
180
+ drawerOpen,
181
+ drawerHovered,
182
+ openDrawer,
183
+ closeDrawer
184
+ } = useApp();
185
+ const isExpanded = drawerOpen;
186
+ const isHovered = drawerHovered && !drawerOpen;
187
+ const showFullContent = isExpanded || isHovered;
188
+
189
+ return (
190
+ <div className="shrink-0 mt-auto px-4 pt-0.5 pb-2">
191
+ <Tooltip
192
+ title={isExpanded ? "Collapse" : "Expand"}
193
+ side="right"
194
+ sideOffset={12}
195
+ open={isHovered ? false : undefined}
196
+ >
197
+ <div
198
+ className={cls(
199
+ "flex flex-row items-center rounded-lg cursor-pointer",
200
+ "hover:bg-surface-accent-100 dark:hover:bg-surface-800",
201
+ "transition-colors duration-150 h-[30px]"
202
+ )}
203
+ role="button"
204
+ tabIndex={0}
205
+ aria-expanded={isExpanded}
206
+ aria-label={isExpanded ? "Collapse" : "Expand"}
207
+ onClick={() => isExpanded ? closeDrawer() : openDrawer()}
208
+ onKeyDown={(e) => {
209
+ if (e.key === "Enter" || e.key === " ") {
210
+ e.preventDefault();
211
+ isExpanded ? closeDrawer() : openDrawer();
212
+ }
213
+ }}
214
+ >
215
+ <div className="shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-surface-400">
216
+ {isExpanded
217
+ ? <KeyboardDoubleArrowLeftIcon size={"small"} />
218
+ : <KeyboardDoubleArrowRightIcon size={"small"} />}
219
+ </div>
220
+ <div className={cls(
221
+ "overflow-hidden transition-all duration-200 ease-in-out",
222
+ showFullContent ? "opacity-100 w-auto" : "opacity-0 w-0"
223
+ )}>
224
+ <Typography
225
+ variant="body2"
226
+ className="text-surface-500 dark:text-surface-400 select-none whitespace-nowrap"
227
+ >
228
+ {isExpanded ? "Collapse" : "Expand"}
229
+ </Typography>
230
+ </div>
231
+ </div>
232
+ </Tooltip>
233
+ </div>
234
+ );
235
+ }
146
236
 
237
+ /**
238
+ * This is the logo displayed in the drawer.
239
+ * A compact logo aligned to the left of the drawer header.
240
+ *
241
+ * @param logo
147
242
  */
148
243
  export function DrawerLogo({ logo }: {
149
244
  logo?: string;
150
245
  }) {
151
246
 
152
247
  const navigation = useNavigationController();
153
- const { drawerOpen } = useApp();
154
- return <div
155
- style={{
156
- transition: "padding 100ms cubic-bezier(0.4, 0, 0.6, 1) 0ms",
157
- padding: drawerOpen ? "32px 144px 0px 24px" : "72px 12px 0px 12px"
158
- }}
159
- className={cls("cursor-pointer rounded ml-3 mr-1")}>
160
-
248
+ return <div className="flex flex-row items-center shrink-0 pt-4 pb-0 px-2">
161
249
  <Tooltip title={"Home"}
162
- sideOffset={20}
163
- side="right">
250
+ sideOffset={20}
251
+ side="right">
164
252
  <Link
165
- className={"block"}
253
+ className={"shrink-0 flex items-center justify-center w-[56px] h-[40px]"}
166
254
  to={navigation.basePath}>
167
255
  {logo
168
256
  ? <img src={logo}
169
- alt="Logo"
170
- className={cls("max-w-full max-h-full transition-all object-contain",
171
- drawerOpen ? "w-[96px] h-[96px]" : "w-[32px] h-[32px]")} />
172
- : <FireCMSLogo />}
173
-
257
+ alt="Logo"
258
+ className={"w-[28px] h-[28px] object-contain"} />
259
+ : <FireCMSLogo width={"28px"} height={"28px"} />}
174
260
  </Link>
175
261
  </Tooltip>
176
262
  </div>;
@@ -62,43 +62,41 @@ export function DrawerNavigationGroup({
62
62
  const { t } = useTranslation();
63
63
  return (
64
64
  <div
65
- className={"bg-surface-50 dark:bg-surface-800/30 my-4 rounded-lg ml-3 mr-1"}
65
+ className={"my-2 mx-2 flex flex-col"}
66
66
  key={`drawer_group_${group}`}
67
67
  >
68
68
  {/* Group Header */}
69
- {drawerOpen ? (
70
- <div
71
- className="pl-4 pr-2 py-2 flex flex-row items-center cursor-pointer hover:bg-surface-100 dark:hover:bg-surface-700/50 rounded-t-lg transition-colors"
72
- onClick={onToggleCollapsed}
73
- >
74
- <ExpandMoreIcon
75
- size={"smallest"}
76
- className={cls(
77
- "text-surface-500 dark:text-surface-400 transition-transform duration-200 mr-1",
78
- collapsed ? "-rotate-90" : "rotate-0"
79
- )}
80
- />
81
- <Typography
82
- variant={"caption"}
83
- color={"secondary"}
84
- className="font-medium flex-grow line-clamp-1"
85
- >
86
- {(group && group !== "__default__" ? group : t("views_group")).toUpperCase()}
87
- </Typography>
88
- {headerActions && (
89
- <div onClick={(e) => e.stopPropagation()}>
90
- {headerActions}
91
- </div>
69
+ <div
70
+ className={cls("pl-3 pr-2 py-0.5 flex flex-row items-center transition-colors",
71
+ drawerOpen ? "cursor-pointer hover:bg-surface-100 dark:hover:bg-surface-800/40 rounded-lg" : "opacity-0 invisible pointer-events-none"
72
+ )}
73
+ onClick={drawerOpen ? onToggleCollapsed : undefined}
74
+ >
75
+ <ExpandMoreIcon
76
+ size={"smallest"}
77
+ className={cls(
78
+ "text-surface-400 dark:text-surface-400 transition-transform duration-200 mr-1",
79
+ collapsed ? "-rotate-90" : "rotate-0"
92
80
  )}
93
- </div>
94
- ) : (
95
- <div className="w-full" />
96
- )}
81
+ />
82
+ <Typography
83
+ variant={"caption"}
84
+ color={"secondary"}
85
+ className="font-semibold text-[11px] uppercase tracking-wider flex-grow line-clamp-1 text-surface-400 dark:text-surface-400"
86
+ >
87
+ {(group && group !== "__default__" ? group : t("views_group"))}
88
+ </Typography>
89
+ {headerActions && (
90
+ <div onClick={(e) => e.stopPropagation()}>
91
+ {headerActions}
92
+ </div>
93
+ )}
94
+ </div>
97
95
 
98
96
  {/* Collapsible Content */}
99
97
  <div
100
98
  className={cls(
101
- "overflow-hidden transition-all duration-200 ease-in-out",
99
+ "transition-all duration-200 ease-in-out overflow-hidden rounded-lg",
102
100
  collapsed ? "max-h-0 opacity-0" : "max-h-[2000px] opacity-100"
103
101
  )}
104
102
  >
@@ -22,7 +22,7 @@ export function DrawerNavigationItem({
22
22
  }) {
23
23
 
24
24
  const iconWrap = <div
25
- className={"text-text-secondary dark:text-text-secondary-dark"}>
25
+ className={"shrink-0 flex items-center justify-center w-[44px] h-[30px] text-surface-500 dark:text-text-secondary-dark group-hover/nav:text-primary transition-colors duration-150"}>
26
26
  {icon}
27
27
  </div>;
28
28
 
@@ -33,14 +33,12 @@ export function DrawerNavigationItem({
33
33
  width: "100%",
34
34
  transition: drawerOpen ? "width 150ms ease-in" : undefined
35
35
  }}
36
- className={({ isActive }: any) => cls("rounded-lg truncate",
37
- "hover:bg-surface-accent-300 hover:bg-opacity-75 hover:bg-surface-accent-300/75 dark:hover:bg-surface-accent-800 dark:hover:bg-opacity-75 dark:hover:bg-surface-accent-800/75 text-text-primary dark:text-surface-200 hover:text-surface-900 hover:dark:text-white hover:bg-surface-accent-300/75 dark:hover:bg-surface-accent-800/75",
38
- "flex flex-row items-center mr-8",
39
- // "transition-all ease-in-out delay-100 duration-300",
40
- // drawerOpen ? "w-full" : "w-18",
41
- drawerOpen ? "pl-4 h-10" : "pl-4 h-9",
42
- "font-semibold text-xs",
43
- isActive ? "bg-surface-accent-200 bg-opacity-60 dark:bg-surface-800 dark:bg-opacity-50 bg-surface-accent-200/60 dark:bg-surface-800/50" : ""
36
+ className={({ isActive }: { isActive: boolean }) => cls("rounded-lg truncate group/nav",
37
+ "hover:bg-primary/5 dark:hover:bg-primary/5 text-surface-700 dark:text-surface-300 hover:text-surface-900 dark:hover:text-white",
38
+ "flex flex-row items-center",
39
+ drawerOpen ? "pr-4 h-[30px]" : "h-[30px]",
40
+ "font-medium text-[13px]",
41
+ isActive ? "bg-primary/8 dark:bg-primary/10 text-primary dark:text-primary [&_div]:text-primary" : ""
44
42
  )}
45
43
  to={url}
46
44
  >
@@ -49,11 +47,11 @@ export function DrawerNavigationItem({
49
47
 
50
48
  <div
51
49
  className={cls(
52
- "text-text-primary dark:text-surface-200",
50
+ "text-surface-700 dark:text-surface-300",
53
51
  drawerOpen ? "opacity-100" : "opacity-0 hidden",
54
- "ml-4 font-inherit"
52
+ "font-inherit truncate space-x-2"
55
53
  )}>
56
- {name.toUpperCase()}
54
+ {name}
57
55
  </div>
58
56
  </NavLink>
59
57
  </div>;
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo, useState } from "react";
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
3
  Entity,
4
4
  EntityCollection,
@@ -13,6 +13,7 @@ import {
13
13
  import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary } from "../components";
14
14
  import {
15
15
  canEditEntity,
16
+ encodeEntityId,
16
17
  removeInitialAndTrailingSlashes,
17
18
  resolveCollection,
18
19
  resolveDefaultSelectedView,
@@ -246,6 +247,47 @@ export function EntityEditViewInner<M extends Record<string, any>>({
246
247
 
247
248
  const authController = useAuthController();
248
249
 
250
+ // Track which tabs have been visited so we keep them mounted (preserving
251
+ // their state) without eagerly mounting tabs that were never opened.
252
+ const mountedTabsRef = useRef<Set<string>>(new Set());
253
+ if (selectedTab) {
254
+ mountedTabsRef.current.add(selectedTab);
255
+ }
256
+
257
+ // Memoize the read-only fallback form context so it is not recreated on
258
+ // every render when there is no real formContext (read-only entity views).
259
+ const readOnlyFormContext = useMemo<FormContext | undefined>(() => {
260
+ if (formContext) return undefined; // real formContext takes precedence
261
+ if (!entityId) return undefined;
262
+ const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
263
+ return {
264
+ entityId,
265
+ disabled: false,
266
+ openEntityMode: layout,
267
+ status: status,
268
+ values: usedEntity?.values ?? {},
269
+ setFieldValue: (key: string, value: any) => {
270
+ throw new Error("You can't update values in read only mode");
271
+ },
272
+ save: () => {
273
+ throw new Error("You can't save in read only mode");
274
+ },
275
+ collection: resolveCollection<M>({
276
+ collection,
277
+ path,
278
+ entityId,
279
+ values: usedEntity?.values ?? {},
280
+ previousValues: usedEntity?.values ?? {},
281
+ propertyConfigs: customizationController.propertyConfigs,
282
+ authController
283
+ }),
284
+ path,
285
+ entity: usedEntity,
286
+ savingError: undefined,
287
+ formex: formexStub
288
+ };
289
+ }, [formContext, entityId, layout, status, usedEntity, collection, path, customizationController.propertyConfigs, authController]);
290
+
249
291
  const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
250
292
  .filter(e => !e.includeActions)
251
293
  .map((customView) => {
@@ -262,38 +304,18 @@ export function EntityEditViewInner<M extends Record<string, any>>({
262
304
  return null;
263
305
  }
264
306
 
265
- const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
266
- const usedFormContext: FormContext = formContext ?? {
267
- entityId,
268
- disabled: false,
269
- openEntityMode: layout,
270
- status: status,
271
- values: usedEntity?.values ?? {},
272
- setFieldValue: (key: string, value: any) => {
273
- throw new Error("You can't update values in read only mode");
274
- },
275
- save: () => {
276
- throw new Error("You can't save in read only mode");
277
- },
278
- collection: resolveCollection<M>({
279
- collection,
280
- path,
281
- entityId,
282
- values: usedEntity?.values ?? {},
283
- previousValues: usedEntity?.values ?? {},
284
- propertyConfigs: customizationController.propertyConfigs,
285
- authController
286
- }),
287
- path,
288
- entity: usedEntity,
289
- savingError: undefined,
290
- formex: formexStub
291
- };
307
+ // Only mount tabs that are active or have been visited before.
308
+ const isActive = selectedTab === customView.key;
309
+ if (!isActive && !mountedTabsRef.current.has(customView.key)) {
310
+ return null;
311
+ }
312
+
313
+ const usedFormContext: FormContext = formContext ?? readOnlyFormContext!;
292
314
 
293
315
  return <div
294
316
  className={cls(defaultBorderMixin,
295
317
  "relative flex-1 w-full h-full overflow-auto",
296
- { "hidden": selectedTab !== customView.key }
318
+ { "hidden": !isActive }
297
319
  )}
298
320
  key={`custom_view_${customView.key}`}
299
321
  role="tabpanel">
@@ -311,7 +333,8 @@ export function EntityEditViewInner<M extends Record<string, any>>({
311
333
 
312
334
  const globalLoading = dataLoading && !usedEntity;
313
335
 
314
- const jsonView = <div
336
+ // Only mount the JSON view once its tab has been selected at least once.
337
+ const jsonView = (selectedTab === JSON_TAB_VALUE || mountedTabsRef.current.has(JSON_TAB_VALUE)) ? <div
315
338
  className={cls("relative flex-1 h-full overflow-auto w-full",
316
339
  { "hidden": selectedTab !== JSON_TAB_VALUE })}
317
340
  key={"json_view"}
@@ -320,12 +343,14 @@ export function EntityEditViewInner<M extends Record<string, any>>({
320
343
  <EntityJsonPreview
321
344
  values={formContext?.values ?? entity?.values ?? {}} />
322
345
  </ErrorBoundary>
323
- </div>;
346
+ </div> : null;
324
347
 
325
348
  const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
326
349
  const subcollectionId = subcollection.id ?? subcollection.path;
350
+ // `newFullPath` addresses the datasource, so the parent id stays raw.
327
351
  const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
328
- const newFullIdPath = fullIdPath ? `${fullIdPath}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
352
+ // `newFullIdPath` is fed to buildUrlCollectionPath downstream, so it stays escaped.
353
+ const newFullIdPath = fullIdPath && usedEntity ? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
329
354
 
330
355
  if (selectedTab !== subcollectionId) return null;
331
356
  return (
@@ -6,6 +6,7 @@ import {
6
6
  ArrayOfReferencesFieldBinding,
7
7
  BlockFieldBinding,
8
8
  DateTimeFieldBinding,
9
+ GeopointFieldBinding,
9
10
  KeyValueFieldBinding,
10
11
  MapFieldBinding,
11
12
  MultiSelectFieldBinding,
@@ -34,6 +35,7 @@ import {
34
35
  ListAltIcon,
35
36
  ListIcon,
36
37
  MailIcon,
38
+ LocationOnIcon,
37
39
  NumbersIcon, PersonIcon,
38
40
  RepeatIcon,
39
41
  ScheduleIcon,
@@ -273,6 +275,17 @@ export const DEFAULT_FIELD_CONFIGS: Record<string, PropertyConfig<any>> = {
273
275
  Field: DateTimeFieldBinding
274
276
  }
275
277
  },
278
+ geopoint: {
279
+ key: "geopoint",
280
+ name: "Geopoint",
281
+ description: "Latitude and longitude pair",
282
+ Icon: LocationOnIcon,
283
+ color: "#0ea5e9",
284
+ property: {
285
+ dataType: "geopoint",
286
+ Field: GeopointFieldBinding
287
+ }
288
+ },
276
289
  group: {
277
290
  key: "group",
278
291
  name: "Group",
@@ -419,6 +432,8 @@ export function getDefaultFieldId(property: Property | ResolvedProperty) {
419
432
  return "switch";
420
433
  } else if (property.dataType === "date") {
421
434
  return "date_time";
435
+ } else if (property.dataType === "geopoint") {
436
+ return "geopoint";
422
437
  } else if (property.dataType === "reference") {
423
438
  return "reference";
424
439
  }
@@ -58,7 +58,7 @@ export function ArrayOfReferencesFieldBinding({
58
58
  }
59
59
 
60
60
  const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
61
- setValue(entities.map(e => getReferenceFrom(e)));
61
+ setValue(entities.filter(Boolean).map(e => getReferenceFrom(e)));
62
62
  }, [setValue]);
63
63
 
64
64
  const referenceDialogController = useReferenceDialog({