@firecms/core 3.2.0 → 3.3.0-canary.451aa49
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/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -1
- package/dist/components/VirtualTable/types.d.ts +1 -0
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +20186 -19539
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +24292 -23645
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +38 -0
- package/dist/types/properties.d.ts +9 -8
- package/dist/types/translations.d.ts +23 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/lazy_eager.d.ts +7 -0
- package/dist/util/objects.d.ts +1 -0
- package/package.json +4 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +9 -3
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +3 -5
- package/src/components/EntityJsonPreview.tsx +2 -1
- package/src/components/ErrorBoundary.tsx +3 -3
- package/src/components/VirtualTable/VirtualTable.tsx +5 -3
- package/src/components/VirtualTable/VirtualTableHeader.tsx +9 -8
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +8 -3
- package/src/components/VirtualTable/VirtualTableProps.tsx +7 -1
- package/src/components/VirtualTable/types.tsx +1 -0
- package/src/core/DrawerNavigationGroup.tsx +1 -1
- package/src/core/EntityEditView.tsx +50 -5
- package/src/core/EntitySidePanel.tsx +2 -1
- package/src/core/field_configs.tsx +4 -2
- package/src/form/EntityForm.tsx +64 -4
- package/src/form/PropertyFieldBinding.tsx +4 -3
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +18 -5
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +18 -5
- package/src/form/field_bindings/BlockFieldBinding.tsx +21 -7
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -6
- package/src/form/field_bindings/MapFieldBinding.tsx +23 -8
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +43 -20
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +15 -1
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +25 -11
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +25 -11
- package/src/form/field_bindings/RepeatFieldBinding.tsx +18 -5
- package/src/form/field_bindings/SelectFieldBinding.tsx +7 -5
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +24 -7
- package/src/form/field_bindings/SwitchFieldBinding.tsx +31 -14
- package/src/form/field_bindings/TextFieldBinding.tsx +10 -7
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +7 -5
- package/src/index.ts +1 -0
- package/src/locales/de.ts +28 -1
- package/src/locales/en.ts +27 -0
- package/src/locales/es.ts +28 -1
- package/src/locales/fr.ts +28 -1
- package/src/locales/hi.ts +28 -1
- package/src/locales/it.ts +28 -1
- package/src/locales/pt.ts +28 -1
- package/src/preview/PropertyPreview.tsx +3 -2
- package/src/preview/components/ReferencePreview.tsx +2 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +49 -27
- package/src/routes/FireCMSRoute.tsx +63 -54
- package/src/types/collections.ts +40 -0
- package/src/types/properties.ts +11 -10
- package/src/types/translations.ts +27 -0
- package/src/util/index.ts +1 -0
- package/src/util/lazy_eager.tsx +33 -0
- package/src/util/objects.ts +15 -0
|
@@ -142,6 +142,30 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
142
142
|
* the side dialog of an entity.
|
|
143
143
|
*/
|
|
144
144
|
subcollections?: EntityCollection<any, any>[];
|
|
145
|
+
/**
|
|
146
|
+
* You can group subcollections and custom views into dropdown menus
|
|
147
|
+
* in the entity view tabs. Views listed in a group will be removed
|
|
148
|
+
* from the top-level tabs and shown under a single dropdown instead.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```tsx
|
|
152
|
+
* const productsCollection = buildCollection({
|
|
153
|
+
* id: "products",
|
|
154
|
+
* path: "products",
|
|
155
|
+
* name: "Products",
|
|
156
|
+
* properties: { ... },
|
|
157
|
+
* subcollections: [localesCollection, reviewsCollection],
|
|
158
|
+
* entityViews: [sampleView],
|
|
159
|
+
* viewGroups: [
|
|
160
|
+
* {
|
|
161
|
+
* name: "Related data",
|
|
162
|
+
* views: ["locales", "reviews", "sample_view"]
|
|
163
|
+
* }
|
|
164
|
+
* ]
|
|
165
|
+
* });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
viewGroups?: ViewGroup[];
|
|
145
169
|
/**
|
|
146
170
|
* This interface defines all the callbacks that can be used when an entity
|
|
147
171
|
* is being created, updated or deleted.
|
|
@@ -361,6 +385,20 @@ export interface KanbanConfig<M extends Record<string, any> = any> {
|
|
|
361
385
|
*/
|
|
362
386
|
columnProperty: Extract<keyof M, string>;
|
|
363
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* You can group subcollections and custom views into dropdown menus in the entity view tabs.
|
|
390
|
+
* @group Collections
|
|
391
|
+
*/
|
|
392
|
+
export interface ViewGroup {
|
|
393
|
+
/**
|
|
394
|
+
* Name of the group
|
|
395
|
+
*/
|
|
396
|
+
name: string;
|
|
397
|
+
/**
|
|
398
|
+
* Array of subcollection paths/ids or custom view keys
|
|
399
|
+
*/
|
|
400
|
+
views: string[];
|
|
401
|
+
}
|
|
364
402
|
/**
|
|
365
403
|
* View mode for displaying a collection.
|
|
366
404
|
* @group Collections
|
|
@@ -125,6 +125,15 @@ export interface BaseProperty<T extends CMSType, CustomProps = any> {
|
|
|
125
125
|
* @see https://jsonlogic.com/ for JSON Logic syntax
|
|
126
126
|
*/
|
|
127
127
|
conditions?: PropertyConditions;
|
|
128
|
+
/**
|
|
129
|
+
* Set this property to true to provide the UX to explicitly set the value to `null`.
|
|
130
|
+
* Defaults to `false`.
|
|
131
|
+
*/
|
|
132
|
+
nullable?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* @deprecated Use `nullable` instead.
|
|
135
|
+
*/
|
|
136
|
+
clearable?: boolean;
|
|
128
137
|
}
|
|
129
138
|
/**
|
|
130
139
|
* @group Entity properties
|
|
@@ -473,10 +482,6 @@ export interface NumberProperty extends BaseProperty<number> {
|
|
|
473
482
|
* Rules for validating this property
|
|
474
483
|
*/
|
|
475
484
|
validation?: NumberPropertyValidationSchema;
|
|
476
|
-
/**
|
|
477
|
-
* Add an icon to clear the value and set it to `null`. Defaults to `false`
|
|
478
|
-
*/
|
|
479
|
-
clearable?: boolean;
|
|
480
485
|
}
|
|
481
486
|
/**
|
|
482
487
|
* @group Entity properties
|
|
@@ -569,10 +574,6 @@ export interface StringProperty extends BaseProperty<string> {
|
|
|
569
574
|
* Rules for validating this property
|
|
570
575
|
*/
|
|
571
576
|
validation?: StringPropertyValidationSchema;
|
|
572
|
-
/**
|
|
573
|
-
* Add an icon to clear the value and set it to `null`. Defaults to `false`
|
|
574
|
-
*/
|
|
575
|
-
clearable?: boolean;
|
|
576
577
|
/**
|
|
577
578
|
* You can use this property (a string) to behave as a reference to another
|
|
578
579
|
* collection. The stored value is the ID of the entity in the
|
|
@@ -412,6 +412,8 @@ export interface FireCMSTranslations {
|
|
|
412
412
|
cms_users: string;
|
|
413
413
|
roles_menu: string;
|
|
414
414
|
project_settings: string;
|
|
415
|
+
firestore_explorer: string;
|
|
416
|
+
explore_your_firestore_data: string;
|
|
415
417
|
build_admin_panel_in_minutes: string;
|
|
416
418
|
go_live_instantly: string;
|
|
417
419
|
create_production_ready_back_offices: string;
|
|
@@ -643,4 +645,25 @@ export interface FireCMSTranslations {
|
|
|
643
645
|
settings_appcheck_refresh_note: string;
|
|
644
646
|
settings_appcheck_updated: string;
|
|
645
647
|
settings_appcheck_error: string;
|
|
648
|
+
missing_firestore_security_rules: string;
|
|
649
|
+
firecms_cloud_requires_security_rule: string;
|
|
650
|
+
cannot_be_accessed_without_it: string;
|
|
651
|
+
required_security_rule: string;
|
|
652
|
+
fix_automatically: string;
|
|
653
|
+
open_firebase_rules: string;
|
|
654
|
+
security_rules_updated_successfully: string;
|
|
655
|
+
sec_rules_fixing: string;
|
|
656
|
+
sec_rules_fixed: string;
|
|
657
|
+
marketplace_managed_by_gcp: string;
|
|
658
|
+
marketplace_billing_note: string;
|
|
659
|
+
marketplace_manage_in_gcp_console: string;
|
|
660
|
+
marketplace_plan_changes_note: string;
|
|
661
|
+
marketplace_welcome_title: string;
|
|
662
|
+
marketplace_welcome_subtitle: string;
|
|
663
|
+
marketplace_select_or_create_project: string;
|
|
664
|
+
marketplace_link_project: string;
|
|
665
|
+
marketplace_linking: string;
|
|
666
|
+
marketplace_link_success: string;
|
|
667
|
+
marketplace_link_error: string;
|
|
668
|
+
marketplace_no_account_id: string;
|
|
646
669
|
}
|
package/dist/util/index.d.ts
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Returns a React.lazy component that is also preloaded immediately using
|
|
4
|
+
* requestIdleCallback or setTimeout.
|
|
5
|
+
* This ensures that chunks are split, but fetched in the background before they are actually needed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function lazyEager<T extends React.ComponentType<any>>(factory: () => Promise<any>, exportName?: string): React.LazyExoticComponent<T>;
|
package/dist/util/objects.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export declare function removeUndefined(value: any, removeEmptyStrings?: boolean
|
|
|
10
10
|
export declare function removeNulls(value: any): any;
|
|
11
11
|
export declare function isEmptyObject(obj: object): boolean;
|
|
12
12
|
export declare function removePropsIfExisting(source: any, comparison: any): any;
|
|
13
|
+
export declare function jsonStringifyReplacer(key: string, value: any): any;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.3.0-canary.451aa49",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@dnd-kit/core": "^6.3.1",
|
|
54
54
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
55
55
|
"@dnd-kit/sortable": "^10.0.0",
|
|
56
|
-
"@firecms/formex": "3.
|
|
57
|
-
"@firecms/ui": "3.
|
|
56
|
+
"@firecms/formex": "^3.3.0-canary.451aa49",
|
|
57
|
+
"@firecms/ui": "^3.3.0-canary.451aa49",
|
|
58
58
|
"@floating-ui/dom": "^1.7.4",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.10",
|
|
60
60
|
"@radix-ui/react-slot": "^1.2.4",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
"dist",
|
|
137
137
|
"src"
|
|
138
138
|
],
|
|
139
|
-
"gitHead": "
|
|
139
|
+
"gitHead": "772b4a7f64893038f0cc13669d6bc66ec858fc49",
|
|
140
140
|
"publishConfig": {
|
|
141
141
|
"access": "public"
|
|
142
142
|
},
|
|
@@ -5,6 +5,7 @@ import { Badge, Checkbox, cls, IconButton, Menu, MenuItem, MoreVertIcon, Skeleto
|
|
|
5
5
|
import { useFireCMSContext, useLargeLayout } from "../../hooks";
|
|
6
6
|
import { getEntityFromCache } from "../../util/entity_cache";
|
|
7
7
|
import { getLocalChangesBackup } from "../../util";
|
|
8
|
+
import { getChanges } from "../../form/EntityForm";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
*
|
|
@@ -81,13 +82,18 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
|
|
|
81
82
|
const collapsedActions = actions.filter(a => a.collapsed || a.collapsed === undefined);
|
|
82
83
|
const uncollapsedActions = actions.filter(a => a.collapsed === false);
|
|
83
84
|
const enableLocalChangesBackup = collection ? getLocalChangesBackup(collection) : false;
|
|
84
|
-
const
|
|
85
|
+
const cachedData = enableLocalChangesBackup ? getEntityFromCache(fullPath + "/" + entity.id) : undefined;
|
|
86
|
+
const hasDraft = (() => {
|
|
87
|
+
if (!cachedData || typeof cachedData !== "object" || Object.keys(cachedData).length === 0) return false;
|
|
88
|
+
const realChanges = getChanges(cachedData as any, (entity?.values ?? {}) as any);
|
|
89
|
+
return Object.keys(realChanges).length > 0;
|
|
90
|
+
})();
|
|
85
91
|
const iconSize = largeLayout && (size === "m" || size === "l" || size == "xl") ? "medium" : "small";
|
|
86
92
|
|
|
87
93
|
const content = (
|
|
88
94
|
<div
|
|
89
95
|
className={cls(
|
|
90
|
-
"h-full flex items-center justify-center flex-col bg-surface-50 dark:bg-surface-900 bg-opacity-90 bg-surface-50/90 dark:bg-opacity-90 dark:bg-surface-900/90 z-10",
|
|
96
|
+
"h-full flex items-center justify-center flex-col bg-surface-50 dark:bg-surface-900 bg-opacity-90 bg-surface-50/90 dark:bg-opacity-90 dark:bg-surface-900/90 z-10 shrink-0",
|
|
91
97
|
frozen ? "sticky left-0" : ""
|
|
92
98
|
)}
|
|
93
99
|
onClick={useCallback((event: any) => {
|
|
@@ -101,7 +107,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
|
|
|
101
107
|
}}>
|
|
102
108
|
|
|
103
109
|
{(hasActions || selectionEnabled) &&
|
|
104
|
-
<div className="w-
|
|
110
|
+
<div className="w-full flex justify-center">
|
|
105
111
|
|
|
106
112
|
{uncollapsedActions.map((action, index) => {
|
|
107
113
|
const isEditAction = action.key === "edit";
|
|
@@ -904,11 +904,9 @@ export const EntityCollectionView = React.memo(
|
|
|
904
904
|
/>
|
|
905
905
|
|
|
906
906
|
{/* View content - only the view-specific content changes */}
|
|
907
|
-
{tableController.dataLoadingError
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
)}
|
|
911
|
-
{viewMode === "kanban" && enabledViews.includes("kanban") ? (
|
|
907
|
+
{tableController.dataLoadingError ? (
|
|
908
|
+
pluginErrorView ?? <CollectionDataErrorBanner error={tableController.dataLoadingError} />
|
|
909
|
+
) : viewMode === "kanban" && enabledViews.includes("kanban") ? (
|
|
912
910
|
<EntityCollectionBoardView
|
|
913
911
|
key={`kanban-view-${fullPath}-${selectedKanbanProperty}`}
|
|
914
912
|
collection={collection}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useRef } from "react";
|
|
2
2
|
import { Highlight, themes } from "prism-react-renderer";
|
|
3
3
|
import { useModeController } from "../hooks";
|
|
4
|
+
import { jsonStringifyReplacer } from "../util/objects";
|
|
4
5
|
|
|
5
6
|
export function EntityJsonPreview({ values }: { values: object }) {
|
|
6
|
-
const code = JSON.stringify(values,
|
|
7
|
+
const code = JSON.stringify(values, jsonStringifyReplacer, "\t");
|
|
7
8
|
const { mode } = useModeController();
|
|
8
9
|
const preRef = useRef<HTMLPreElement>(null);
|
|
9
10
|
|
|
@@ -35,10 +35,10 @@ export class ErrorBoundary extends React.Component<PropsWithChildren<Record<stri
|
|
|
35
35
|
function FallbackView({ message }: { message?: string }) {
|
|
36
36
|
const { t } = useTranslation();
|
|
37
37
|
return (
|
|
38
|
-
<div className="h-full w-full bg-slate-100 flex items-center justify-center p-4">
|
|
38
|
+
<div className="h-full w-full bg-slate-100 dark:bg-surface-900 flex items-center justify-center p-4">
|
|
39
39
|
<div
|
|
40
|
-
className="flex flex-col items-center justify-center m-4 bg-white dark:bg-
|
|
41
|
-
<div className="flex items-center mb-4 text-red-500">
|
|
40
|
+
className="flex flex-col items-center justify-center m-4 bg-white dark:bg-surface-800 p-8 rounded-lg shadow-sm border border-gray-200 dark:border-surface-700">
|
|
41
|
+
<div className="flex items-center mb-4 text-red-500 dark:text-red-400">
|
|
42
42
|
<ErrorIcon/>
|
|
43
43
|
<div className="ml-4">{t("error")}</div>
|
|
44
44
|
</div>
|
|
@@ -126,6 +126,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
126
126
|
AddColumnComponent,
|
|
127
127
|
initialScroll = 0,
|
|
128
128
|
onColumnsOrderChange,
|
|
129
|
+
headerIconSize,
|
|
129
130
|
}: VirtualTableProps<T>) {
|
|
130
131
|
|
|
131
132
|
const sortByProperty: string | undefined = sortBy ? sortBy[0] : undefined;
|
|
@@ -211,7 +212,7 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
211
212
|
}, [tableRef]);
|
|
212
213
|
|
|
213
214
|
const [measureRef, bounds] = useMeasure({
|
|
214
|
-
debounce:
|
|
215
|
+
debounce: 0,
|
|
215
216
|
polyfill: ResizeObserver,
|
|
216
217
|
scroll: true,
|
|
217
218
|
// This is important for handling zooming in react-flow
|
|
@@ -369,8 +370,9 @@ export const VirtualTable = React.memo<VirtualTableProps<any>>(
|
|
|
369
370
|
setColumns(newColumns);
|
|
370
371
|
onColumnsOrderChange(newColumns);
|
|
371
372
|
} : undefined,
|
|
372
|
-
draggingColumnId
|
|
373
|
-
|
|
373
|
+
draggingColumnId,
|
|
374
|
+
headerIconSize,
|
|
375
|
+
}), [data, rowHeight, cellRenderer, columns, currentSort, onRowClick, customView, onColumnResizeInternal, onColumnResizeEndInternal, filterInput, onColumnSort, onFilterUpdateInternal, sortByProperty, hoverRow, createFilterField, rowClassName, endAdornment, AddColumnComponent, onColumnsOrderChange, draggingColumnId, headerIconSize]);
|
|
374
376
|
|
|
375
377
|
// Get sortable column keys (excluding frozen columns)
|
|
376
378
|
const sortableColumnKeys = columns
|
|
@@ -48,6 +48,7 @@ type VirtualTableHeaderProps<M extends Record<string, any>> = {
|
|
|
48
48
|
AdditionalHeaderWidget?: (props: { onHover: boolean }) => React.ReactNode;
|
|
49
49
|
isDragging?: boolean;
|
|
50
50
|
isDraggable?: boolean;
|
|
51
|
+
headerIconSize?: "small" | "smallest";
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
|
|
@@ -64,7 +65,8 @@ export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
|
|
|
64
65
|
createFilterField,
|
|
65
66
|
AdditionalHeaderWidget,
|
|
66
67
|
isDragging,
|
|
67
|
-
isDraggable
|
|
68
|
+
isDraggable,
|
|
69
|
+
headerIconSize = "small",
|
|
68
70
|
}: VirtualTableHeaderProps<M>) {
|
|
69
71
|
|
|
70
72
|
const [onHover, setOnHover] = useState(false);
|
|
@@ -136,18 +138,17 @@ export const VirtualTableHeader = React.memo<VirtualTableHeaderProps<any>>(
|
|
|
136
138
|
<Badge color="secondary"
|
|
137
139
|
invisible={!sort}>
|
|
138
140
|
<IconButton
|
|
139
|
-
size={
|
|
141
|
+
size={headerIconSize}
|
|
140
142
|
className={onHover || openFilter ? "bg-white dark:bg-surface-950" : undefined}
|
|
141
143
|
onClick={() => {
|
|
142
144
|
onColumnSort(column.key as Extract<keyof M, string>);
|
|
143
145
|
}}
|
|
144
146
|
>
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<ArrowUpwardIcon className={"rotate-180"} />}
|
|
147
|
+
<ArrowUpwardIcon size={headerIconSize}
|
|
148
|
+
className={cls(
|
|
149
|
+
"transition-transform duration-200",
|
|
150
|
+
sort === "desc" ? "rotate-180" : "rotate-0"
|
|
151
|
+
)} />
|
|
151
152
|
</IconButton>
|
|
152
153
|
</Badge>
|
|
153
154
|
}
|
|
@@ -22,7 +22,8 @@ const SortableColumnHeader = ({
|
|
|
22
22
|
onClickResizeColumn,
|
|
23
23
|
createFilterField,
|
|
24
24
|
isDragging,
|
|
25
|
-
isDraggable
|
|
25
|
+
isDraggable,
|
|
26
|
+
headerIconSize
|
|
26
27
|
}: {
|
|
27
28
|
column: VirtualTableColumn;
|
|
28
29
|
columnIndex: number;
|
|
@@ -37,6 +38,7 @@ const SortableColumnHeader = ({
|
|
|
37
38
|
createFilterField: any;
|
|
38
39
|
isDragging: boolean;
|
|
39
40
|
isDraggable: boolean;
|
|
41
|
+
headerIconSize?: "small" | "smallest";
|
|
40
42
|
}) => {
|
|
41
43
|
const [isPressing, setIsPressing] = useState(false);
|
|
42
44
|
|
|
@@ -103,7 +105,8 @@ const SortableColumnHeader = ({
|
|
|
103
105
|
createFilterField={createFilterField}
|
|
104
106
|
AdditionalHeaderWidget={column.AdditionalHeaderWidget}
|
|
105
107
|
isDragging={isDragging || isPressing}
|
|
106
|
-
isDraggable={isDraggable}
|
|
108
|
+
isDraggable={isDraggable}
|
|
109
|
+
headerIconSize={headerIconSize} />
|
|
107
110
|
</div>
|
|
108
111
|
);
|
|
109
112
|
};
|
|
@@ -123,7 +126,8 @@ export const VirtualTableHeaderRow = ({
|
|
|
123
126
|
data,
|
|
124
127
|
cellRenderer: CellRenderer,
|
|
125
128
|
rowHeight = 54,
|
|
126
|
-
draggingColumnId
|
|
129
|
+
draggingColumnId,
|
|
130
|
+
headerIconSize,
|
|
127
131
|
}: VirtualTableContextProps<any>) => {
|
|
128
132
|
|
|
129
133
|
const columnRefs = useMemo(() => columns.map(() => createRef<HTMLDivElement>()), [columns.length]);
|
|
@@ -234,6 +238,7 @@ export const VirtualTableHeaderRow = ({
|
|
|
234
238
|
createFilterField={createFilterField}
|
|
235
239
|
isDragging={isDragging}
|
|
236
240
|
isDraggable={isDraggable}
|
|
241
|
+
headerIconSize={headerIconSize}
|
|
237
242
|
/>
|
|
238
243
|
</ErrorBoundary>
|
|
239
244
|
);
|
|
@@ -168,6 +168,12 @@ export interface VirtualTableProps<T extends Record<string, any>> {
|
|
|
168
168
|
*/
|
|
169
169
|
onColumnsOrderChange?: (columns: VirtualTableColumn[]) => void;
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Size of icons in column headers (sort, filter).
|
|
173
|
+
* @default "small"
|
|
174
|
+
*/
|
|
175
|
+
headerIconSize?: "small" | "smallest";
|
|
176
|
+
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
export type CellRendererParams<T = any> = {
|
|
@@ -206,7 +212,7 @@ export interface VirtualTableColumn<CustomProps = any> {
|
|
|
206
212
|
/**
|
|
207
213
|
* Label displayed in the header
|
|
208
214
|
*/
|
|
209
|
-
title?:
|
|
215
|
+
title?: React.ReactNode;
|
|
210
216
|
|
|
211
217
|
/**
|
|
212
218
|
* This column is frozen to the left
|
|
@@ -83,7 +83,7 @@ export function DrawerNavigationGroup({
|
|
|
83
83
|
color={"secondary"}
|
|
84
84
|
className="font-medium flex-grow line-clamp-1"
|
|
85
85
|
>
|
|
86
|
-
{(group
|
|
86
|
+
{(group && group !== "__default__" ? group : t("views_group")).toUpperCase()}
|
|
87
87
|
</Typography>
|
|
88
88
|
{headerActions && (
|
|
89
89
|
<div onClick={(e) => e.stopPropagation()}>
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
useFireCMSContext,
|
|
27
27
|
useLargeLayout
|
|
28
28
|
} from "../hooks";
|
|
29
|
-
import { CircularProgress, cls, CodeIcon, defaultBorderMixin, Tab, Tabs, Typography } from "@firecms/ui";
|
|
29
|
+
import { CircularProgress, cls, CodeIcon, defaultBorderMixin, Tab, Tabs, Typography, Menu, MenuItem, ExpandMoreIcon } from "@firecms/ui";
|
|
30
30
|
import { getEntityFromMemoryCache } from "../util/entity_cache";
|
|
31
31
|
import { EntityForm, EntityFormProps } from "../form";
|
|
32
32
|
import { EntityEditViewFormActions } from "./EntityEditViewFormActions";
|
|
@@ -230,6 +230,10 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
230
230
|
const includeJsonView = collection.includeJsonView === undefined ? true : collection.includeJsonView;
|
|
231
231
|
const hasAdditionalViews = customViewsCount > 0 || subcollectionsCount > 0 || includeJsonView;
|
|
232
232
|
|
|
233
|
+
const groupedViews = useMemo(() => {
|
|
234
|
+
return (collection.viewGroups ?? []).flatMap(g => g.views);
|
|
235
|
+
}, [collection.viewGroups]);
|
|
236
|
+
|
|
233
237
|
const {
|
|
234
238
|
resolvedEntityViews,
|
|
235
239
|
selectedEntityView,
|
|
@@ -419,16 +423,18 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
419
423
|
Builder={selectedSecondaryForm?.Builder}
|
|
420
424
|
/>;
|
|
421
425
|
|
|
422
|
-
const subcollectionTabs = subcollections && subcollections
|
|
426
|
+
const subcollectionTabs = subcollections && subcollections
|
|
427
|
+
.filter(sub => !groupedViews.includes(sub.id ?? sub.path))
|
|
428
|
+
.map((subcollection) =>
|
|
423
429
|
<Tab
|
|
424
430
|
className="text-sm min-w-[120px]"
|
|
425
|
-
value={subcollection.id}
|
|
431
|
+
value={subcollection.id ?? subcollection.path}
|
|
426
432
|
key={`entity_detail_collection_tab_${subcollection.name}`}>
|
|
427
433
|
{subcollection.name}
|
|
428
434
|
</Tab>
|
|
429
435
|
);
|
|
430
436
|
|
|
431
|
-
const customViewTabsStart = resolvedEntityViews.filter(view => view.position === "start")
|
|
437
|
+
const customViewTabsStart = resolvedEntityViews.filter(view => view.position === "start" && !groupedViews.includes(view.key))
|
|
432
438
|
.map((view) =>
|
|
433
439
|
<Tab
|
|
434
440
|
className={!view.tabComponent ? "text-sm min-w-[120px]" : undefined}
|
|
@@ -437,7 +443,7 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
437
443
|
{view.tabComponent ?? view.name}
|
|
438
444
|
</Tab>
|
|
439
445
|
);
|
|
440
|
-
const customViewTabsEnd = resolvedEntityViews.filter(view => !view.position || view.position === "end")
|
|
446
|
+
const customViewTabsEnd = resolvedEntityViews.filter(view => (!view.position || view.position === "end") && !groupedViews.includes(view.key))
|
|
441
447
|
.map((view) =>
|
|
442
448
|
<Tab
|
|
443
449
|
className={!view.tabComponent ? "text-sm min-w-[120px]" : undefined}
|
|
@@ -447,6 +453,43 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
447
453
|
</Tab>
|
|
448
454
|
);
|
|
449
455
|
|
|
456
|
+
const viewGroupMenus = collection.viewGroups?.map(group => {
|
|
457
|
+
const isActive = group.views.includes(selectedTab);
|
|
458
|
+
return (
|
|
459
|
+
<Menu
|
|
460
|
+
key={`view_group_${group.name}`}
|
|
461
|
+
trigger={
|
|
462
|
+
<button
|
|
463
|
+
type="button"
|
|
464
|
+
className={cls(
|
|
465
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-white transition-all",
|
|
466
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-surface-400 focus-visible:ring-offset-2",
|
|
467
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
468
|
+
isActive ? "bg-white text-surface-900 dark:bg-surface-950 dark:text-surface-50" : "text-surface-600 dark:text-surface-400 hover:bg-surface-200 dark:hover:bg-surface-800"
|
|
469
|
+
)}
|
|
470
|
+
>
|
|
471
|
+
{group.name}
|
|
472
|
+
<ExpandMoreIcon className="ml-1 -mr-1" size="small" />
|
|
473
|
+
</button>
|
|
474
|
+
}>
|
|
475
|
+
{group.views.map(viewId => {
|
|
476
|
+
const subcollection = subcollections.find(s => (s.id ?? s.path) === viewId);
|
|
477
|
+
const customView = resolvedEntityViews.find(v => v.key === viewId);
|
|
478
|
+
const name = subcollection?.name ?? customView?.name ?? viewId;
|
|
479
|
+
return (
|
|
480
|
+
<MenuItem
|
|
481
|
+
key={`view_group_${group.name}_${viewId}`}
|
|
482
|
+
onClick={() => onSideTabClick(viewId)}
|
|
483
|
+
className={selectedTab === viewId ? "bg-surface-accent-100 dark:bg-surface-accent-900" : ""}
|
|
484
|
+
>
|
|
485
|
+
{name}
|
|
486
|
+
</MenuItem>
|
|
487
|
+
);
|
|
488
|
+
})}
|
|
489
|
+
</Menu>
|
|
490
|
+
);
|
|
491
|
+
});
|
|
492
|
+
|
|
450
493
|
const shouldShowTopBar = Boolean(barActions) || hasAdditionalViews;
|
|
451
494
|
|
|
452
495
|
let result = <div className="relative flex flex-col h-full w-full bg-white dark:bg-surface-900">
|
|
@@ -494,6 +537,8 @@ export function EntityEditViewInner<M extends Record<string, any>>({
|
|
|
494
537
|
|
|
495
538
|
{customViewTabsEnd}
|
|
496
539
|
|
|
540
|
+
{viewGroupMenus}
|
|
541
|
+
|
|
497
542
|
{subcollectionTabs}
|
|
498
543
|
</Tabs>}
|
|
499
544
|
</div>}
|
|
@@ -78,7 +78,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
78
78
|
return navigationController.getParentCollectionIds(path);
|
|
79
79
|
}, [navigationController, path]);
|
|
80
80
|
|
|
81
|
-
const collection = navigationController.getCollection(fullIdPath ?? path)
|
|
81
|
+
const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
|
|
82
82
|
|
|
83
83
|
useEffect(() => {
|
|
84
84
|
function beforeunload(e: any) {
|
|
@@ -112,6 +112,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
|
|
|
112
112
|
return (
|
|
113
113
|
<>
|
|
114
114
|
<ErrorBoundary>
|
|
115
|
+
|
|
115
116
|
<EntityEditView
|
|
116
117
|
{...props}
|
|
117
118
|
fullIdPath={fullIdPath}
|
|
@@ -8,16 +8,18 @@ import {
|
|
|
8
8
|
DateTimeFieldBinding,
|
|
9
9
|
KeyValueFieldBinding,
|
|
10
10
|
MapFieldBinding,
|
|
11
|
-
MarkdownEditorFieldBinding,
|
|
12
11
|
MultiSelectFieldBinding,
|
|
13
12
|
ReferenceAsStringFieldBinding,
|
|
14
13
|
ReferenceFieldBinding,
|
|
15
14
|
RepeatFieldBinding,
|
|
16
15
|
SelectFieldBinding,
|
|
17
|
-
StorageUploadFieldBinding,
|
|
18
16
|
SwitchFieldBinding,
|
|
19
17
|
TextFieldBinding
|
|
20
18
|
} from "../form";
|
|
19
|
+
import { lazyEager } from "../util/lazy_eager";
|
|
20
|
+
|
|
21
|
+
const MarkdownEditorFieldBinding = lazyEager<typeof import("../form/field_bindings/MarkdownEditorFieldBinding")["MarkdownEditorFieldBinding"]>(() => import("../form/field_bindings/MarkdownEditorFieldBinding"), "MarkdownEditorFieldBinding");
|
|
22
|
+
const StorageUploadFieldBinding = lazyEager<typeof import("../form/field_bindings/StorageUploadFieldBinding")["StorageUploadFieldBinding"]>(() => import("../form/field_bindings/StorageUploadFieldBinding"), "StorageUploadFieldBinding");
|
|
21
23
|
import { isPropertyBuilder, mergeDeep } from "../util";
|
|
22
24
|
|
|
23
25
|
import {
|