@firecms/core 3.3.0 → 3.4.0-canary.f6a889a
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 +1979 -721
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1978 -720
- 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/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 +34 -44
- 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/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/i18n/FireCMSi18nProvider.tsx +2 -0
- package/src/internal/useBuildSideEntityController.tsx +2 -1
- package/src/locales/de.ts +2 -2
- package/src/locales/en.ts +2 -2
- package/src/locales/es.ts +2 -2
- package/src/locales/fr.ts +2 -2
- package/src/locales/hi.ts +2 -2
- package/src/locales/it.ts +2 -2
- package/src/locales/pl.ts +730 -0
- package/src/locales/pt.ts +2 -2
- 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/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
|
@@ -200,43 +200,44 @@ export type EntityPreviewContainerProps = {
|
|
|
200
200
|
onClick?: (e: React.SyntheticEvent) => void;
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
export
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
203
|
+
export function EntityPreviewContainer({
|
|
204
|
+
children,
|
|
205
|
+
hover,
|
|
206
|
+
onClick,
|
|
207
|
+
size = "medium",
|
|
208
|
+
style,
|
|
209
|
+
className,
|
|
210
|
+
fullwidth = true,
|
|
211
|
+
ref
|
|
212
|
+
}: EntityPreviewContainerProps & { ref?: React.Ref<HTMLDivElement> }) {
|
|
213
|
+
const divClassName = cls(
|
|
214
|
+
"bg-white dark:bg-surface-900",
|
|
215
|
+
"min-h-[44px]",
|
|
216
|
+
fullwidth ? "w-full" : "",
|
|
217
|
+
"items-center",
|
|
218
|
+
hover ? "hover:bg-surface-accent-50 dark:hover:bg-surface-800 group-hover:bg-surface-accent-50 dark:group-hover:bg-surface-800" : "",
|
|
219
|
+
size === "small" ? "p-1" : "px-2 py-1",
|
|
220
|
+
"flex border rounded-lg",
|
|
221
|
+
onClick ? "cursor-pointer" : "",
|
|
222
|
+
defaultBorderMixin,
|
|
223
|
+
className
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const handleClick = onClick
|
|
227
|
+
? (event: React.MouseEvent<HTMLDivElement>) => {
|
|
228
|
+
event.preventDefault();
|
|
229
|
+
onClick(event);
|
|
230
|
+
}
|
|
231
|
+
: undefined;
|
|
232
|
+
|
|
233
|
+
const divProps: React.HTMLAttributes<HTMLDivElement> & { ref?: React.Ref<HTMLDivElement> } = {
|
|
234
|
+
ref,
|
|
235
|
+
tabIndex: 0,
|
|
236
|
+
style,
|
|
237
|
+
className: divClassName,
|
|
238
|
+
onClick: handleClick,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return <div {...divProps}>{children}</div>;
|
|
242
|
+
}
|
|
243
|
+
|
|
@@ -73,7 +73,7 @@ export function ReferenceWidget<M extends Record<string, any>>({
|
|
|
73
73
|
if (disabled)
|
|
74
74
|
return;
|
|
75
75
|
if (onMultipleReferenceSelected) {
|
|
76
|
-
const references = entities ? entities.map(e => getReferenceFrom(e)) : null;
|
|
76
|
+
const references = entities ? entities.filter(Boolean).map(e => getReferenceFrom(e)) : null;
|
|
77
77
|
onMultipleReferenceSelected({
|
|
78
78
|
references,
|
|
79
79
|
entities
|
|
@@ -113,11 +113,11 @@ export function ReferenceFilterField({
|
|
|
113
113
|
}, [path]);
|
|
114
114
|
|
|
115
115
|
const onSingleEntitySelected = (entity: Entity<any>) => {
|
|
116
|
-
updateFilter(operation, getReferenceFrom(entity));
|
|
116
|
+
if (entity) updateFilter(operation, getReferenceFrom(entity));
|
|
117
117
|
};
|
|
118
118
|
|
|
119
119
|
const onMultipleEntitiesSelected = (entities: Entity<any>[]) => {
|
|
120
|
-
updateFilter(operation, entities.map(e => getReferenceFrom(e)));
|
|
120
|
+
updateFilter(operation, entities.filter(Boolean).map(e => getReferenceFrom(e)));
|
|
121
121
|
};
|
|
122
122
|
|
|
123
123
|
const multiple = multipleSelectOperations.includes(operation);
|
|
@@ -135,7 +135,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
135
135
|
const [columns, setColumns] = useState(columnsProp);
|
|
136
136
|
|
|
137
137
|
const tableRef = useRef<HTMLDivElement>(null);
|
|
138
|
-
const
|
|
138
|
+
const lastEndReachedDataLength = useRef<number | undefined>(undefined);
|
|
139
139
|
|
|
140
140
|
const debouncedScroll = useDebounceCallback(onScrollProp, 200);
|
|
141
141
|
|
|
@@ -242,7 +242,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
242
242
|
}, [filterInput]);
|
|
243
243
|
|
|
244
244
|
const scrollToTop = useCallback(() => {
|
|
245
|
-
|
|
245
|
+
lastEndReachedDataLength.current = undefined;
|
|
246
246
|
if (tableRef.current) {
|
|
247
247
|
tableRef.current.scrollTo(tableRef.current?.scrollLeft, 0);
|
|
248
248
|
}
|
|
@@ -278,13 +278,24 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
278
278
|
|
|
279
279
|
const maxScroll = Math.max((data?.length ?? 0) * rowHeight - bounds.height, 0);
|
|
280
280
|
|
|
281
|
-
const onEndReachedInternal = useCallback((
|
|
282
|
-
|
|
283
|
-
|
|
281
|
+
const onEndReachedInternal = useCallback(() => {
|
|
282
|
+
const dataLength = data?.length ?? 0;
|
|
283
|
+
|
|
284
|
+
if (onEndReached && dataLength > 0 && lastEndReachedDataLength.current !== dataLength) {
|
|
285
|
+
lastEndReachedDataLength.current = dataLength;
|
|
284
286
|
onEndReached();
|
|
285
287
|
}
|
|
286
288
|
}, [data?.length, onEndReached]);
|
|
287
289
|
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (!onEndReached || loading || !data?.length || !bounds.height || !tableRef.current) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (tableRef.current.scrollHeight <= tableRef.current.clientHeight) {
|
|
295
|
+
onEndReachedInternal();
|
|
296
|
+
}
|
|
297
|
+
}, [bounds.height, data?.length, loading, onEndReached, onEndReachedInternal]);
|
|
298
|
+
|
|
288
299
|
const onScroll = useCallback(({
|
|
289
300
|
scrollDirection,
|
|
290
301
|
scrollOffset,
|
|
@@ -302,12 +313,12 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
302
313
|
})
|
|
303
314
|
}
|
|
304
315
|
if (!scrollUpdateWasRequested && (scrollOffset >= maxScroll - endOffset))
|
|
305
|
-
onEndReachedInternal(
|
|
306
|
-
}, [maxScroll, onEndReachedInternal]);
|
|
316
|
+
onEndReachedInternal();
|
|
317
|
+
}, [endOffset, maxScroll, onEndReachedInternal]);
|
|
307
318
|
|
|
308
319
|
const onFilterUpdateInternal = useCallback((column: VirtualTableColumn, filterForProperty?: [VirtualTableWhereFilterOp, any]) => {
|
|
309
320
|
|
|
310
|
-
|
|
321
|
+
lastEndReachedDataLength.current = undefined;
|
|
311
322
|
const filter = filterRef.current;
|
|
312
323
|
let newFilterValue: VirtualTableFilterValues<any> = filter ? { ...filter } : {};
|
|
313
324
|
|
|
@@ -4,6 +4,7 @@ import { useLocation } from "react-router-dom";
|
|
|
4
4
|
import { useDataSource, useFireCMSContext, useNavigationController } from "../../hooks";
|
|
5
5
|
import { useDataOrder } from "../../hooks/data/useDataOrder";
|
|
6
6
|
import {
|
|
7
|
+
DataType,
|
|
7
8
|
Entity,
|
|
8
9
|
EntityCollection,
|
|
9
10
|
EntityReference,
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
} from "../../types";
|
|
17
18
|
import { useDebouncedData } from "./useDebouncedData";
|
|
18
19
|
import { ScrollRestorationController } from "./useScrollRestoration";
|
|
20
|
+
import { isDataTypeFilterable } from "../../util";
|
|
21
|
+
import { decodeEntityId, encodeEntityId } from "../../util/navigation_utils";
|
|
19
22
|
|
|
20
23
|
const DEFAULT_PAGE_SIZE = 50;
|
|
21
24
|
|
|
@@ -135,8 +138,41 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
135
138
|
filterValues: initialFilterUrl,
|
|
136
139
|
sortBy: initialSortUrl,
|
|
137
140
|
} = parseFilterAndSort(location.search);
|
|
141
|
+
|
|
142
|
+
const availableFilterKeys = collection.allowedFilters ?? Object.keys(collection.properties);
|
|
143
|
+
const forcedFilterKeys = collection.forceFilter ? Object.keys(collection.forceFilter) : [];
|
|
138
144
|
|
|
139
|
-
const
|
|
145
|
+
const allowedFilterKeys = useMemo(() => {
|
|
146
|
+
const availableKeys = availableFilterKeys.filter((key) => {
|
|
147
|
+
const property = collection.properties[key];
|
|
148
|
+
|
|
149
|
+
if (!property) return false;
|
|
150
|
+
|
|
151
|
+
if (typeof property === "function") return false;
|
|
152
|
+
|
|
153
|
+
const dataType = property.dataType;
|
|
154
|
+
const filterable = dataType === "array"
|
|
155
|
+
? isDataTypeFilterable((property as { of?: { dataType: DataType } }).of?.dataType as DataType, true)
|
|
156
|
+
: isDataTypeFilterable(dataType);
|
|
157
|
+
|
|
158
|
+
return filterable;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const forcedKeys = forcedFilterKeys.filter((key) => !availableKeys.includes(key));
|
|
162
|
+
|
|
163
|
+
return [...availableKeys, ...forcedKeys];
|
|
164
|
+
|
|
165
|
+
}, [collection.properties, availableFilterKeys, forcedFilterKeys]);
|
|
166
|
+
|
|
167
|
+
const removeUnallowedFilters = useCallback((filters?: FilterValues<Extract<keyof M, string>>) => {
|
|
168
|
+
if (!filters) return;
|
|
169
|
+
|
|
170
|
+
return Object.fromEntries(Object.entries(filters).filter(([key]) => allowedFilterKeys.includes(key as keyof M))) as FilterValues<Extract<keyof M, string>>;
|
|
171
|
+
}, [allowedFilterKeys]);
|
|
172
|
+
|
|
173
|
+
const initFilters = forceFilter ?? (updateUrl ? initialFilterUrl : undefined) ?? initialFilter ?? undefined
|
|
174
|
+
|
|
175
|
+
const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string>> | undefined>(removeUnallowedFilters(initFilters));
|
|
140
176
|
const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string>, "asc" | "desc"] | undefined>((updateUrl ? initialSortUrl : undefined) ?? initialSortInternal);
|
|
141
177
|
|
|
142
178
|
useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
|
|
@@ -168,7 +204,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
168
204
|
const [dataLoadingError, setDataLoadingError] = useState<Error | undefined>();
|
|
169
205
|
const [noMoreToLoad, setNoMoreToLoad] = useState<boolean>(false);
|
|
170
206
|
|
|
171
|
-
const clearFilter = useCallback(() => setFilterValues(forceFilter
|
|
207
|
+
const clearFilter = useCallback(() => setFilterValues(removeUnallowedFilters(forceFilter)), [forceFilter, removeUnallowedFilters]);
|
|
172
208
|
|
|
173
209
|
const updateFilterValues = useCallback((updatedFilter: FilterValues<Extract<keyof M, string>> | undefined) => {
|
|
174
210
|
if (forceFilter) {
|
|
@@ -178,9 +214,9 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
178
214
|
if (updatedFilter && Object.keys(updatedFilter).length === 0) {
|
|
179
215
|
setFilterValues(undefined);
|
|
180
216
|
} else {
|
|
181
|
-
setFilterValues(updatedFilter);
|
|
217
|
+
setFilterValues(removeUnallowedFilters(updatedFilter));
|
|
182
218
|
}
|
|
183
|
-
}, [forceFilter]);
|
|
219
|
+
}, [forceFilter, removeUnallowedFilters]);
|
|
184
220
|
|
|
185
221
|
useEffect(() => {
|
|
186
222
|
|
|
@@ -268,6 +304,8 @@ export function useDataSourceTableController<M extends Record<string, any> = any
|
|
|
268
304
|
dataLoadingError,
|
|
269
305
|
filterValues,
|
|
270
306
|
setFilterValues: updateFilterValues,
|
|
307
|
+
allowedFilters: allowedFilterKeys,
|
|
308
|
+
forcedFilters: forcedFilterKeys,
|
|
271
309
|
sortBy,
|
|
272
310
|
setSortBy,
|
|
273
311
|
searchString,
|
|
@@ -342,7 +380,13 @@ function encodeFilterAndSort(filterValues?: FilterValues<string>, sortBy?: [stri
|
|
|
342
380
|
}
|
|
343
381
|
if (encodedValue !== undefined) {
|
|
344
382
|
entries[encodeURIComponent(`${key}_op`)] = encodeURIComponent(op);
|
|
345
|
-
|
|
383
|
+
// Note: check for null/undefined explicitly instead of truthiness,
|
|
384
|
+
// otherwise falsy-but-valid values (boolean `false`, number `0`)
|
|
385
|
+
// would be serialized as "null" and round-trip back as `null`,
|
|
386
|
+
// breaking filters like `archived == false`.
|
|
387
|
+
entries[encodeURIComponent(`${key}_value`)] = encodedValue !== null && encodedValue !== undefined
|
|
388
|
+
? encodeURIComponent(encodedValue.toString())
|
|
389
|
+
: "null";
|
|
346
390
|
}
|
|
347
391
|
}
|
|
348
392
|
});
|
|
@@ -394,7 +438,21 @@ function isDate(dateString: string): boolean {
|
|
|
394
438
|
}
|
|
395
439
|
|
|
396
440
|
function encodeRef(val: EntityReference) {
|
|
397
|
-
return `ref::${val.path}/${val.id}`;
|
|
441
|
+
return `ref::${val.path}/${encodeEntityId(val.id)}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Split a "path/id" reference on its LAST separator: the path may itself be a
|
|
446
|
+
* subcollection path with several segments, and the id is escaped so it is always exactly
|
|
447
|
+
* one segment.
|
|
448
|
+
*/
|
|
449
|
+
function decodeRef(encoded: string): EntityReference {
|
|
450
|
+
const separatorIndex = encoded.lastIndexOf("/");
|
|
451
|
+
if (separatorIndex < 0) return new EntityReference(encoded, "");
|
|
452
|
+
return new EntityReference(
|
|
453
|
+
decodeEntityId(encoded.substring(separatorIndex + 1)),
|
|
454
|
+
encoded.substring(0, separatorIndex)
|
|
455
|
+
);
|
|
398
456
|
}
|
|
399
457
|
|
|
400
458
|
function decodeString(val: string): EntityReference | Date | string {
|
|
@@ -410,8 +468,7 @@ function decodeString(val: string): EntityReference | Date | string {
|
|
|
410
468
|
try {
|
|
411
469
|
parsedFilterVal = JSON.parse(parsedFilterVal, (key, value) => {
|
|
412
470
|
if (typeof value === "string" && value.startsWith("ref::")) {
|
|
413
|
-
|
|
414
|
-
return new EntityReference(id, path);
|
|
471
|
+
return decodeRef(value.substring(5));
|
|
415
472
|
}
|
|
416
473
|
return value;
|
|
417
474
|
});
|
|
@@ -421,8 +478,7 @@ function decodeString(val: string): EntityReference | Date | string {
|
|
|
421
478
|
}
|
|
422
479
|
|
|
423
480
|
if (typeof parsedFilterVal === "string" && parsedFilterVal.startsWith("ref::")) {
|
|
424
|
-
|
|
425
|
-
return new EntityReference(id, path);
|
|
481
|
+
return decodeRef(parsedFilterVal.substring(5));
|
|
426
482
|
}
|
|
427
483
|
return parsedFilterVal;
|
|
428
484
|
}
|
|
@@ -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>;
|