@firecms/core 3.3.0 → 3.4.0-canary.2575a08
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 +8 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- 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 +9 -1
- package/dist/components/common/useTableSearchHelper.d.ts +9 -1
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- 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 +9 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +3658 -1820
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +3655 -1817
- 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 +31 -0
- package/dist/types/datasource.d.ts +67 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +13 -0
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -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/entity_cache.d.ts +11 -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 +107 -3
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- 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/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- 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 +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +45 -41
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- 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/default_entity_actions.tsx +22 -2
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +126 -163
- package/src/components/common/useTableSearchHelper.ts +26 -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 +77 -35
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -4
- package/src/core/field_configs.tsx +15 -0
- package/src/form/EntityForm.tsx +31 -10
- package/src/form/EntityFormActions.tsx +2 -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/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +6 -1
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +35 -4
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +50 -4
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +37 -15
- package/src/internal/useBuildSideEntityController.tsx +20 -5
- 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/ReferencePreview.tsx +7 -3
- 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 +54 -27
- package/src/types/collections.ts +34 -0
- package/src/types/datasource.ts +67 -4
- package/src/types/entities.ts +24 -1
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +13 -0
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -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/entity_cache.ts +18 -0
- 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 +47 -7
- package/src/util/navigation_utils.ts +244 -4
- package/src/util/parent_references_from_path.ts +45 -4
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
- 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,
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
|
|
13
13
|
import { CircularProgressCenter, EntityCollectionView, EntityView, ErrorBoundary } from "../components";
|
|
14
14
|
import {
|
|
15
|
+
buildSubcollectionPathSegments,
|
|
15
16
|
canEditEntity,
|
|
17
|
+
encodeEntityId,
|
|
16
18
|
removeInitialAndTrailingSlashes,
|
|
17
19
|
resolveCollection,
|
|
18
20
|
resolveDefaultSelectedView,
|
|
@@ -27,7 +29,7 @@ import {
|
|
|
27
29
|
useLargeLayout
|
|
28
30
|
} from "../hooks";
|
|
29
31
|
import { CircularProgress, cls, CodeIcon, defaultBorderMixin, Tab, Tabs, Typography, Menu, MenuItem, ExpandMoreIcon } from "@firecms/ui";
|
|
30
|
-
import { getEntityFromMemoryCache } from "../util/entity_cache";
|
|
32
|
+
import { entityCacheKey, getEntityFromMemoryCache } from "../util/entity_cache";
|
|
31
33
|
import { EntityForm, EntityFormProps } from "../form";
|
|
32
34
|
import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
|
|
33
35
|
import { EntityJsonPreview } from "../components/EntityJsonPreview";
|
|
@@ -41,6 +43,8 @@ export type OnUpdateParams = {
|
|
|
41
43
|
entity: Entity<any>,
|
|
42
44
|
status: EntityStatus,
|
|
43
45
|
path: string,
|
|
46
|
+
/** `path` split at its real segment boundaries, when known. Never derived from `path`. */
|
|
47
|
+
pathSegments?: string[],
|
|
44
48
|
entityId?: string;
|
|
45
49
|
selectedTab?: string;
|
|
46
50
|
collection: EntityCollection<any>
|
|
@@ -70,6 +74,12 @@ export interface EntityEditViewProps<M extends Record<string, any>> {
|
|
|
70
74
|
* The navigation path to the entity.
|
|
71
75
|
*/
|
|
72
76
|
fullIdPath?: string;
|
|
77
|
+
/**
|
|
78
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
79
|
+
* Forwarded to the datasource, and extended for any subcollection rendered inside this
|
|
80
|
+
* view, so a parent entity id containing "/" stays unambiguous.
|
|
81
|
+
*/
|
|
82
|
+
pathSegments?: string[];
|
|
73
83
|
collection: EntityCollection<M>;
|
|
74
84
|
entityId?: string;
|
|
75
85
|
databaseId?: string;
|
|
@@ -100,6 +110,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
100
110
|
dataLoadingError
|
|
101
111
|
} = useEntityFetch<M, USER>({
|
|
102
112
|
path: props.path,
|
|
113
|
+
pathSegments: props.pathSegments,
|
|
103
114
|
entityId: entityId,
|
|
104
115
|
collection: props.collection,
|
|
105
116
|
databaseId: props.databaseId,
|
|
@@ -107,7 +118,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
107
118
|
});
|
|
108
119
|
|
|
109
120
|
const initialDirtyValues = entityId
|
|
110
|
-
? getEntityFromMemoryCache(props.path
|
|
121
|
+
? getEntityFromMemoryCache(entityCacheKey(props.path, entityId))
|
|
111
122
|
: getEntityFromMemoryCache(props.path + "#new");
|
|
112
123
|
|
|
113
124
|
const authController = useAuthController();
|
|
@@ -119,7 +130,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
119
130
|
if (status === "new" || status === "copy") {
|
|
120
131
|
return true;
|
|
121
132
|
} else {
|
|
122
|
-
return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null) : undefined;
|
|
133
|
+
return entity ? canEditEntity(props.collection, authController, props.path, entity ?? null, props.pathSegments) : undefined;
|
|
123
134
|
}
|
|
124
135
|
}, [authController, entity, status]);
|
|
125
136
|
|
|
@@ -145,6 +156,7 @@ export function EntityEditView<M extends Record<string, any>, USER extends User>
|
|
|
145
156
|
export function EntityEditViewInner<M extends Record<string, any>>({
|
|
146
157
|
path,
|
|
147
158
|
fullIdPath,
|
|
159
|
+
pathSegments,
|
|
148
160
|
entityId,
|
|
149
161
|
selectedTab: selectedTabProp,
|
|
150
162
|
collection,
|
|
@@ -193,6 +205,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
193
205
|
entityId,
|
|
194
206
|
parentCollectionIds,
|
|
195
207
|
path,
|
|
208
|
+
pathSegments,
|
|
196
209
|
status,
|
|
197
210
|
collection,
|
|
198
211
|
context,
|
|
@@ -246,6 +259,47 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
246
259
|
|
|
247
260
|
const authController = useAuthController();
|
|
248
261
|
|
|
262
|
+
// Track which tabs have been visited so we keep them mounted (preserving
|
|
263
|
+
// their state) without eagerly mounting tabs that were never opened.
|
|
264
|
+
const mountedTabsRef = useRef<Set<string>>(new Set());
|
|
265
|
+
if (selectedTab) {
|
|
266
|
+
mountedTabsRef.current.add(selectedTab);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Memoize the read-only fallback form context so it is not recreated on
|
|
270
|
+
// every render when there is no real formContext (read-only entity views).
|
|
271
|
+
const readOnlyFormContext = useMemo<FormContext | undefined>(() => {
|
|
272
|
+
if (formContext) return undefined; // real formContext takes precedence
|
|
273
|
+
if (!entityId) return undefined;
|
|
274
|
+
const formexStub = createFormexStub<M>(usedEntity?.values ?? {} as M);
|
|
275
|
+
return {
|
|
276
|
+
entityId,
|
|
277
|
+
disabled: false,
|
|
278
|
+
openEntityMode: layout,
|
|
279
|
+
status: status,
|
|
280
|
+
values: usedEntity?.values ?? {},
|
|
281
|
+
setFieldValue: (key: string, value: any) => {
|
|
282
|
+
throw new Error("You can't update values in read only mode");
|
|
283
|
+
},
|
|
284
|
+
save: () => {
|
|
285
|
+
throw new Error("You can't save in read only mode");
|
|
286
|
+
},
|
|
287
|
+
collection: resolveCollection<M>({
|
|
288
|
+
collection,
|
|
289
|
+
path,
|
|
290
|
+
entityId,
|
|
291
|
+
values: usedEntity?.values ?? {},
|
|
292
|
+
previousValues: usedEntity?.values ?? {},
|
|
293
|
+
propertyConfigs: customizationController.propertyConfigs,
|
|
294
|
+
authController
|
|
295
|
+
}),
|
|
296
|
+
path,
|
|
297
|
+
entity: usedEntity,
|
|
298
|
+
savingError: undefined,
|
|
299
|
+
formex: formexStub
|
|
300
|
+
};
|
|
301
|
+
}, [formContext, entityId, layout, status, usedEntity, collection, path, customizationController.propertyConfigs, authController]);
|
|
302
|
+
|
|
249
303
|
const customViewsView: React.ReactNode[] | undefined = customViews && resolvedEntityViews
|
|
250
304
|
.filter(e => !e.includeActions)
|
|
251
305
|
.map((customView) => {
|
|
@@ -262,38 +316,18 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
262
316
|
return null;
|
|
263
317
|
}
|
|
264
318
|
|
|
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
|
-
};
|
|
319
|
+
// Only mount tabs that are active or have been visited before.
|
|
320
|
+
const isActive = selectedTab === customView.key;
|
|
321
|
+
if (!isActive && !mountedTabsRef.current.has(customView.key)) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const usedFormContext: FormContext = formContext ?? readOnlyFormContext!;
|
|
292
326
|
|
|
293
327
|
return <div
|
|
294
328
|
className={cls(defaultBorderMixin,
|
|
295
329
|
"relative flex-1 w-full h-full overflow-auto",
|
|
296
|
-
{ "hidden":
|
|
330
|
+
{ "hidden": !isActive }
|
|
297
331
|
)}
|
|
298
332
|
key={`custom_view_${customView.key}`}
|
|
299
333
|
role="tabpanel">
|
|
@@ -311,7 +345,8 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
311
345
|
|
|
312
346
|
const globalLoading = dataLoading && !usedEntity;
|
|
313
347
|
|
|
314
|
-
|
|
348
|
+
// Only mount the JSON view once its tab has been selected at least once.
|
|
349
|
+
const jsonView = (selectedTab === JSON_TAB_VALUE || mountedTabsRef.current.has(JSON_TAB_VALUE)) ? <div
|
|
315
350
|
className={cls("relative flex-1 h-full overflow-auto w-full",
|
|
316
351
|
{ "hidden": selectedTab !== JSON_TAB_VALUE })}
|
|
317
352
|
key={"json_view"}
|
|
@@ -320,12 +355,17 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
320
355
|
<EntityJsonPreview
|
|
321
356
|
values={formContext?.values ?? entity?.values ?? {}} />
|
|
322
357
|
</ErrorBoundary>
|
|
323
|
-
</div
|
|
358
|
+
</div> : null;
|
|
324
359
|
|
|
325
360
|
const subCollectionsViews = subcollections && subcollections.map((subcollection) => {
|
|
326
361
|
const subcollectionId = subcollection.id ?? subcollection.path;
|
|
362
|
+
// `newFullPath` addresses the datasource, so the parent id stays raw.
|
|
327
363
|
const newFullPath = usedEntity ? `${path}/${usedEntity?.id}/${removeInitialAndTrailingSlashes(subcollection.path)}` : undefined;
|
|
328
|
-
|
|
364
|
+
// `newFullIdPath` is fed to buildUrlCollectionPath downstream, so it stays escaped.
|
|
365
|
+
const newFullIdPath = fullIdPath && usedEntity ? `${fullIdPath}/${encodeEntityId(usedEntity.id)}/${removeInitialAndTrailingSlashes(subcollectionId)}` : undefined;
|
|
366
|
+
// Only extendable when we were given real segments — deriving the parent chain
|
|
367
|
+
// from the flattened `path` would drop the boundary of a slash-bearing parent id.
|
|
368
|
+
const newPathSegments = buildSubcollectionPathSegments(pathSegments, usedEntity?.id, subcollection.path);
|
|
329
369
|
|
|
330
370
|
if (selectedTab !== subcollectionId) return null;
|
|
331
371
|
return (
|
|
@@ -340,6 +380,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
340
380
|
(usedEntity && newFullPath
|
|
341
381
|
? <EntityCollectionView
|
|
342
382
|
fullPath={newFullPath}
|
|
383
|
+
pathSegments={newPathSegments}
|
|
343
384
|
fullIdPath={newFullIdPath}
|
|
344
385
|
parentCollectionIds={[...parentCollectionIds, collection.id]}
|
|
345
386
|
isSubCollection={true}
|
|
@@ -389,6 +430,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
389
430
|
fullIdPath={fullIdPath}
|
|
390
431
|
collection={collection}
|
|
391
432
|
path={path}
|
|
433
|
+
pathSegments={pathSegments}
|
|
392
434
|
entityId={entityId ?? usedEntity?.id}
|
|
393
435
|
onValuesModified={onValuesModified}
|
|
394
436
|
entity={entity}
|
|
@@ -39,6 +39,7 @@ import { ErrorTooltip } from "../components/ErrorTooltip";
|
|
|
39
39
|
export function EntityEditViewFormActions({
|
|
40
40
|
collection,
|
|
41
41
|
path,
|
|
42
|
+
pathSegments,
|
|
42
43
|
entity,
|
|
43
44
|
layout,
|
|
44
45
|
savingError,
|
|
@@ -63,8 +64,8 @@ export function EntityEditViewFormActions({
|
|
|
63
64
|
const customEntityActions = (collection.entityActions ?? [])
|
|
64
65
|
.map(action => resolveEntityAction(action, customizationController.entityActions))
|
|
65
66
|
.filter(Boolean) as EntityAction[];
|
|
66
|
-
const createEnabled = canCreateEntity(collection, authController, path, null);
|
|
67
|
-
const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity) : false;
|
|
67
|
+
const createEnabled = canCreateEntity(collection, authController, path, null, pathSegments);
|
|
68
|
+
const deleteEnabled = entity ? canDeleteEntity(collection, authController, path, entity, entity.pathSegments ?? pathSegments) : false;
|
|
68
69
|
const actions: EntityAction[] = [];
|
|
69
70
|
if (createEnabled)
|
|
70
71
|
actions.push(copyEntityAction);
|