@firecms/core 3.3.0 → 3.4.0-canary.0ef7442
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/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +7 -0
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
- package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
- package/dist/components/common/table_url_params.d.ts +24 -0
- package/dist/components/common/useDataSourceTableController.d.ts +7 -1
- package/dist/components/common/useTableSearchHelper.d.ts +7 -1
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +7 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +6 -1
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +6 -1
- package/dist/index.es.js +2915 -1330
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2913 -1328
- 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 +24 -0
- package/dist/types/datasource.d.ts +59 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +6 -0
- package/dist/types/translations.d.ts +6 -0
- package/dist/util/__tests__/collections.test.d.ts +1 -0
- package/dist/util/__tests__/urls.test.d.ts +1 -0
- package/dist/util/collections.d.ts +1 -1
- package/dist/util/entities.d.ts +1 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +2 -0
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +17 -0
- package/dist/util/navigation_utils.d.ts +23 -1
- package/dist/util/parent_references_from_path.d.ts +1 -0
- package/dist/util/urls.d.ts +22 -0
- package/package.json +17 -10
- package/src/app/Scaffold.tsx +34 -44
- package/src/app/useApp.tsx +1 -0
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +4 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +29 -3
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +11 -0
- package/src/components/EntityPreview.tsx +41 -40
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +11 -0
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
- package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
- package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
- package/src/components/VirtualTable/VirtualTable.tsx +19 -8
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +104 -159
- package/src/components/common/useTableSearchHelper.ts +21 -2
- package/src/contexts/SnackbarProvider.tsx +14 -1
- 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 +78 -32
- package/src/core/EntitySidePanel.tsx +2 -0
- package/src/core/field_configs.tsx +15 -0
- package/src/form/EntityForm.tsx +9 -3
- 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/data/delete.ts +2 -0
- package/src/hooks/data/save.ts +2 -0
- package/src/hooks/data/useCollectionFetch.tsx +13 -0
- package/src/hooks/data/useEntityFetch.tsx +18 -0
- package/src/hooks/useResolvedNavigationFrom.tsx +2 -0
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +2 -0
- package/src/internal/useBuildDataSource.ts +25 -4
- package/src/internal/useBuildSideEntityController.tsx +8 -1
- package/src/locales/de.ts +7 -2
- package/src/locales/en.ts +7 -2
- package/src/locales/es.ts +7 -2
- package/src/locales/fr.ts +7 -2
- package/src/locales/hi.ts +7 -2
- package/src/locales/it.ts +7 -2
- package/src/locales/pl.ts +735 -0
- package/src/locales/pt.ts +7 -2
- package/src/preview/PropertyPreview.tsx +12 -0
- package/src/preview/components/StorageThumbnail.tsx +24 -2
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +47 -25
- package/src/types/collections.ts +26 -0
- package/src/types/datasource.ts +59 -4
- package/src/types/entities.ts +24 -1
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +6 -0
- package/src/types/translations.ts +6 -0
- package/src/util/__tests__/collections.test.ts +32 -0
- package/src/util/__tests__/urls.test.ts +131 -0
- package/src/util/collections.ts +1 -1
- package/src/util/entities.ts +16 -1
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +2 -0
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +41 -6
- package/src/util/navigation_utils.ts +40 -2
- package/src/util/parent_references_from_path.ts +14 -4
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -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,
|
|
@@ -70,6 +71,12 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
70
71
|
* The navigation path to the entity.
|
|
71
72
|
*/
|
|
72
73
|
fullIdPath?: string;
|
|
74
|
+
/**
|
|
75
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
76
|
+
* Forwarded to the datasource, and extended for any subcollection rendered inside this
|
|
77
|
+
* view, so a parent entity id containing "/" stays unambiguous.
|
|
78
|
+
*/
|
|
79
|
+
pathSegments?: string[];
|
|
73
80
|
collection: EntityCollection<M>;
|
|
74
81
|
entityId?: string;
|
|
75
82
|
databaseId?: string;
|
|
@@ -100,6 +107,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
100
107
|
dataLoadingError
|
|
101
108
|
} = useEntityFetch<M, USER>({
|
|
102
109
|
path: props.path,
|
|
110
|
+
pathSegments: props.pathSegments,
|
|
103
111
|
entityId: entityId,
|
|
104
112
|
collection: props.collection,
|
|
105
113
|
databaseId: props.databaseId,
|
|
@@ -145,6 +153,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
145
153
|
export function EntityEditViewInner<M extends Record<string, any>>({
|
|
146
154
|
path,
|
|
147
155
|
fullIdPath,
|
|
156
|
+
pathSegments,
|
|
148
157
|
entityId,
|
|
149
158
|
selectedTab: selectedTabProp,
|
|
150
159
|
collection,
|
|
@@ -246,6 +255,47 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
246
255
|
|
|
247
256
|
const authController = useAuthController();
|
|
248
257
|
|
|
258
|
+
// Track which tabs have been visited so we keep them mounted (preserving
|
|
259
|
+
// their state) without eagerly mounting tabs that were never opened.
|
|
260
|
+
const mountedTabsRef = useRef<Set<string>>(new Set());
|
|
261
|
+
if (selectedTab) {
|
|
262
|
+
mountedTabsRef.current.add(selectedTab);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Memoize the read-only fallback form context so it is not recreated on
|
|
266
|
+
// every render when there is no real formContext (read-only entity views).
|
|
267
|
+
const readOnlyFormContext = useMemo<FormContext | undefined>(() => {
|
|
268
|
+
if (formContext) return undefined; // real formContext takes precedence
|
|
269
|
+
if (!entityId) return undefined;
|
|
270
|
+
const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
|
|
271
|
+
return {
|
|
272
|
+
entityId,
|
|
273
|
+
disabled: false,
|
|
274
|
+
openEntityMode: layout,
|
|
275
|
+
status: status,
|
|
276
|
+
values: usedEntity?.values ?? {},
|
|
277
|
+
setFieldValue: (key: string, value: any) => {
|
|
278
|
+
throw new Error("You can't update values in read only mode");
|
|
279
|
+
},
|
|
280
|
+
save: () => {
|
|
281
|
+
throw new Error("You can't save in read only mode");
|
|
282
|
+
},
|
|
283
|
+
collection: resolveCollection<M>({
|
|
284
|
+
collection,
|
|
285
|
+
path,
|
|
286
|
+
entityId,
|
|
287
|
+
values: usedEntity?.values ?? {},
|
|
288
|
+
previousValues: usedEntity?.values ?? {},
|
|
289
|
+
propertyConfigs: customizationController.propertyConfigs,
|
|
290
|
+
authController
|
|
291
|
+
}),
|
|
292
|
+
path,
|
|
293
|
+
entity: usedEntity,
|
|
294
|
+
savingError: undefined,
|
|
295
|
+
formex: formexStub
|
|
296
|
+
};
|
|
297
|
+
}, [formContext, entityId, layout, status, usedEntity, collection, path, customizationController.propertyConfigs, authController]);
|
|
298
|
+
|
|
249
299
|
const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
250
300
|
.filter(e => !e.includeActions)
|
|
251
301
|
.map((customView) => {
|
|
@@ -262,38 +312,18 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
262
312
|
return null;
|
|
263
313
|
}
|
|
264
314
|
|
|
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
|
-
};
|
|
315
|
+
// Only mount tabs that are active or have been visited before.
|
|
316
|
+
const isActive = selectedTab === customView.key;
|
|
317
|
+
if (!isActive && !mountedTabsRef.current.has(customView.key)) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const usedFormContext: FormContext = formContext ?? readOnlyFormContext!;
|
|
292
322
|
|
|
293
323
|
return <div
|
|
294
324
|
className={cls(defaultBorderMixin,
|
|
295
325
|
"relative flex-1 w-full h-full overflow-auto",
|
|
296
|
-
{ "hidden":
|
|
326
|
+
{ "hidden": !isActive }
|
|
297
327
|
)}
|
|
298
328
|
key={`custom_view_${customView.key}`}
|
|
299
329
|
role="tabpanel">
|
|
@@ -311,7 +341,8 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
311
341
|
|
|
312
342
|
const globalLoading = dataLoading && !usedEntity;
|
|
313
343
|
|
|
314
|
-
|
|
344
|
+
// Only mount the JSON view once its tab has been selected at least once.
|
|
345
|
+
const jsonView = (selectedTab === JSON_TAB_VALUE || mountedTabsRef.current.has(JSON_TAB_VALUE)) ? <div
|
|
315
346
|
className={cls("relative flex-1 h-full overflow-auto w-full",
|
|
316
347
|
{ "hidden": selectedTab !== JSON_TAB_VALUE })}
|
|
317
348
|
key={"json_view"}
|
|
@@ -320,12 +351,25 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
320
351
|
<EntityJsonPreview
|
|
321
352
|
values={formContext?.values ?? entity?.values ?? {}} />
|
|
322
353
|
</ErrorBoundary>
|
|
323
|
-
</div
|
|
354
|
+
</div> : null;
|
|
324
355
|
|
|
325
356
|
const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
|
|
326
357
|
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
358
|
+
// `newFullPath` addresses the datasource, so the parent id stays raw.
|
|
327
359
|
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
|
|
328
|
-
|
|
360
|
+
// `newFullIdPath` is fed to buildUrlCollectionPath downstream, so it stays escaped.
|
|
361
|
+
const newFullIdPath = fullIdPath && usedEntity ? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
362
|
+
// The subcollection sits under this entity, so its segments are ours plus this
|
|
363
|
+
// entity's id (kept whole, slashes and all) plus the subcollection's own path.
|
|
364
|
+
// Only extendable when we were given real segments — deriving the parent chain
|
|
365
|
+
// from the flattened `path` would drop the boundary of a slash-bearing parent id.
|
|
366
|
+
const newPathSegments = usedEntity && pathSegments
|
|
367
|
+
? [
|
|
368
|
+
...pathSegments,
|
|
369
|
+
usedEntity.id,
|
|
370
|
+
...removeInitialAndTrailingSlashes(subcollection.path).split("/")
|
|
371
|
+
]
|
|
372
|
+
: undefined;
|
|
329
373
|
|
|
330
374
|
if (selectedTab !== subcollectionId) return null;
|
|
331
375
|
return (
|
|
@@ -340,6 +384,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
340
384
|
(usedEntity && newFullPath
|
|
341
385
|
? <EntityCollectionView
|
|
342
386
|
fullPath={newFullPath}
|
|
387
|
+
pathSegments={newPathSegments}
|
|
343
388
|
fullIdPath={newFullIdPath}
|
|
344
389
|
parentCollectionIds={[...parentCollectionIds, collection.id]}
|
|
345
390
|
isSubCollection={true}
|
|
@@ -389,6 +434,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
389
434
|
fullIdPath={fullIdPath}
|
|
390
435
|
collection={collection}
|
|
391
436
|
path={path}
|
|
437
|
+
pathSegments={pathSegments}
|
|
392
438
|
entityId={entityId ?? usedEntity?.id}
|
|
393
439
|
onValuesModified={onValuesModified}
|
|
394
440
|
entity={entity}
|
|
@@ -25,6 +25,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
25
25
|
path,
|
|
26
26
|
entityId,
|
|
27
27
|
fullIdPath,
|
|
28
|
+
pathSegments,
|
|
28
29
|
formProps,
|
|
29
30
|
} = props;
|
|
30
31
|
|
|
@@ -116,6 +117,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
116
117
|
<EntityEditView
|
|
117
118
|
{...props}
|
|
118
119
|
fullIdPath={fullIdPath}
|
|
120
|
+
pathSegments={pathSegments}
|
|
119
121
|
layout={"side_panel"}
|
|
120
122
|
collection={collection as EntityCollection}
|
|
121
123
|
parentCollectionIds={parentCollectionIds}
|
|
@@ -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
|
}
|