@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.
- package/dist/app/useApp.d.ts +1 -0
- package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/index.es.js +2090 -743
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2089 -742
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/pl.d.ts +2 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
- package/dist/types/collections.d.ts +17 -0
- package/dist/types/translations.d.ts +9 -1
- package/dist/util/entities.d.ts +1 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +10 -0
- package/dist/util/navigation_utils.d.ts +20 -0
- package/package.json +16 -9
- package/src/app/Scaffold.tsx +47 -45
- package/src/app/useApp.tsx +1 -0
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +2 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +5 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityPreview.tsx +41 -40
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
- package/src/components/VirtualTable/VirtualTable.tsx +19 -8
- package/src/components/common/useDataSourceTableController.tsx +66 -10
- package/src/core/DefaultDrawer.tsx +150 -64
- package/src/core/DrawerNavigationGroup.tsx +27 -29
- package/src/core/DrawerNavigationItem.tsx +10 -12
- package/src/core/EntityEditView.tsx +57 -32
- package/src/core/field_configs.tsx +15 -0
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
- package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
- package/src/form/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +5 -1
- package/src/i18n/FireCMSi18nProvider.tsx +2 -0
- package/src/internal/useBuildSideEntityController.tsx +2 -1
- package/src/locales/de.ts +11 -3
- package/src/locales/en.ts +11 -3
- package/src/locales/es.ts +11 -3
- package/src/locales/fr.ts +11 -3
- package/src/locales/hi.ts +11 -3
- package/src/locales/it.ts +11 -3
- package/src/locales/pl.ts +730 -0
- package/src/locales/pt.ts +11 -3
- package/src/preview/PropertyPreview.tsx +12 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +44 -25
- package/src/types/collections.ts +18 -0
- package/src/types/translations.ts +9 -1
- package/src/util/entities.ts +13 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +1 -0
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +23 -6
- package/src/util/navigation_utils.ts +36 -2
- package/src/util/parent_references_from_path.ts +4 -2
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
"mx-4 font-inherit text-inherit"
|
|
146
|
+
"font-semibold text-[11px] uppercase tracking-wider text-surface-400"
|
|
118
147
|
)}>
|
|
119
|
-
|
|
148
|
+
{t("admin")}
|
|
120
149
|
</div>}
|
|
121
|
-
</
|
|
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
|
-
*
|
|
143
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
250
|
+
sideOffset={20}
|
|
251
|
+
side="right">
|
|
164
252
|
<Link
|
|
165
|
-
className={"
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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={"
|
|
65
|
+
className={"my-2 mx-2 flex flex-col"}
|
|
66
66
|
key={`drawer_group_${group}`}
|
|
67
67
|
>
|
|
68
68
|
{/* Group Header */}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
"
|
|
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={"
|
|
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 }:
|
|
37
|
-
"hover:bg-
|
|
38
|
-
"flex flex-row items-center
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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-
|
|
50
|
+
"text-surface-700 dark:text-surface-300",
|
|
53
51
|
drawerOpen ? "opacity-100" : "opacity-0 hidden",
|
|
54
|
-
"
|
|
52
|
+
"font-inherit truncate space-x-2"
|
|
55
53
|
)}>
|
|
56
|
-
{name
|
|
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
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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({
|