@firecms/core 3.0.0-rc.1 → 3.0.0-rc.2
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/UserDisplay.d.ts +7 -0
- package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
- package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
- package/dist/core/FireCMS.d.ts +0 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useCollapsedGroups.d.ts +9 -0
- package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
- package/dist/index.es.js +617 -208
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +615 -206
- package/dist/index.umd.js.map +1 -1
- package/dist/preview/components/UserPreview.d.ts +8 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/types/collections.d.ts +2 -0
- package/dist/types/entities.d.ts +5 -1
- package/dist/types/firecms.d.ts +15 -0
- package/dist/types/firecms_context.d.ts +16 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/internal_user_management.d.ts +20 -0
- package/dist/types/plugins.d.ts +2 -0
- package/dist/types/properties.d.ts +9 -0
- package/dist/types/property_config.d.ts +1 -1
- package/dist/types/user.d.ts +1 -1
- package/package.json +5 -5
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
- package/src/components/ErrorView.tsx +1 -1
- package/src/components/HomePage/DefaultHomePage.tsx +9 -26
- package/src/components/HomePage/HomePageDnD.tsx +3 -45
- package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
- package/src/components/PropertyConfigBadge.tsx +2 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -1
- package/src/components/UserDisplay.tsx +55 -0
- package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
- package/src/components/common/useColumnsIds.tsx +1 -8
- package/src/contexts/InternalUserManagementContext.tsx +4 -0
- package/src/core/FireCMS.tsx +22 -13
- package/src/core/field_configs.tsx +15 -1
- package/src/form/PropertyFieldBinding.tsx +4 -0
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
- package/src/hooks/index.tsx +3 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
- package/src/hooks/useCollapsedGroups.ts +64 -0
- package/src/hooks/useFireCMSContext.tsx +6 -2
- package/src/hooks/useInternalUserManagementController.tsx +16 -0
- package/src/preview/PropertyPreview.tsx +8 -0
- package/src/preview/components/ReferencePreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +27 -0
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
- package/src/types/collections.ts +2 -0
- package/src/types/entities.ts +7 -1
- package/src/types/firecms.tsx +16 -0
- package/src/types/firecms_context.tsx +17 -0
- package/src/types/index.ts +1 -0
- package/src/types/internal_user_management.ts +24 -0
- package/src/types/plugins.tsx +3 -0
- package/src/types/properties.ts +10 -0
- package/src/types/property_config.tsx +1 -0
- package/src/types/user.ts +1 -1
- package/src/util/entities.ts +1 -1
- package/src/util/entity_cache.ts +2 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PropertyPreviewProps } from "../PropertyPreviewProps";
|
|
2
|
+
/**
|
|
3
|
+
* Preview component for displaying user information.
|
|
4
|
+
* This is a simple wrapper around UserDisplay.
|
|
5
|
+
*
|
|
6
|
+
* @group Preview components
|
|
7
|
+
*/
|
|
8
|
+
export declare function UserPreview({ value }: PropertyPreviewProps<string>): import("react/jsx-runtime").JSX.Element;
|
package/dist/preview/index.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
|
|
|
60
60
|
* https://fonts.google.com/icons
|
|
61
61
|
* e.g. 'account_tree' or 'person'.
|
|
62
62
|
* Find all the icons in https://firecms.co/docs/icons
|
|
63
|
+
* You can also pass a React node if you want to render a custom icon.
|
|
64
|
+
* If not specified, a default icon will be used.
|
|
63
65
|
*/
|
|
64
66
|
icon?: string | React.ReactNode;
|
|
65
67
|
/**
|
package/dist/types/entities.d.ts
CHANGED
|
@@ -42,7 +42,11 @@ export declare class EntityReference {
|
|
|
42
42
|
* to the root of the database).
|
|
43
43
|
*/
|
|
44
44
|
readonly path: string;
|
|
45
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Optional database ID where the entity is stored (if multiple databases are used)
|
|
47
|
+
*/
|
|
48
|
+
readonly databaseId?: string;
|
|
49
|
+
constructor(id: string, path: string, databaseId?: string);
|
|
46
50
|
get pathWithId(): string;
|
|
47
51
|
isEntityReference(): boolean;
|
|
48
52
|
}
|
package/dist/types/firecms.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { UserConfigurationPersistence } from "./local_config_persistence";
|
|
|
13
13
|
import { FireCMSPlugin } from "./plugins";
|
|
14
14
|
import { CMSAnalyticsEvent } from "./analytics";
|
|
15
15
|
import { EntityAction } from "./entity_actions";
|
|
16
|
+
import { InternalUserManagement } from "./internal_user_management";
|
|
16
17
|
/**
|
|
17
18
|
* Use this callback to build entity collections dynamically.
|
|
18
19
|
* You can use the user to decide which collections to show.
|
|
@@ -123,6 +124,7 @@ export type FireCMSProps<USER extends User> = {
|
|
|
123
124
|
* Use plugins to modify the behaviour of the CMS.
|
|
124
125
|
* DEPRECATED: use the `plugins` prop in the `useBuildNavigationController` instead.
|
|
125
126
|
* This prop will work as a fallback for the `plugins` prop in the `useBuildNavigationController`.
|
|
127
|
+
* @deprecated
|
|
126
128
|
*/
|
|
127
129
|
plugins?: FireCMSPlugin<any, any, any>[];
|
|
128
130
|
/**
|
|
@@ -134,6 +136,19 @@ export type FireCMSProps<USER extends User> = {
|
|
|
134
136
|
* The function must return a URL that gets opened when the button is clicked
|
|
135
137
|
*/
|
|
136
138
|
entityLinkBuilder?: EntityLinkBuilder;
|
|
139
|
+
/**
|
|
140
|
+
* You can use this props to provide your own user management implementation.
|
|
141
|
+
* Note that this will not affect the UI, but it will be used to show user information
|
|
142
|
+
* in various places of the CMS, for example, to show who created or modified an entity,
|
|
143
|
+
* or to assign ownership of an entity.
|
|
144
|
+
*
|
|
145
|
+
* You can also use this data to be retrieved in your custom properties,
|
|
146
|
+
* for example, to show a list of users in a dropdown.
|
|
147
|
+
*
|
|
148
|
+
* If you are using the FireCMS user management plugin, this
|
|
149
|
+
* prop will be implemented automatically.
|
|
150
|
+
*/
|
|
151
|
+
userManagement?: InternalUserManagement;
|
|
137
152
|
components?: {
|
|
138
153
|
/**
|
|
139
154
|
* Component to render when a reference is missing
|
|
@@ -10,6 +10,7 @@ import { SideDialogsController } from "./side_dialogs_controller";
|
|
|
10
10
|
import { DialogsController } from "./dialogs_controller";
|
|
11
11
|
import { CustomizationController } from "./customization_controller";
|
|
12
12
|
import { AnalyticsController } from "./analytics_controller";
|
|
13
|
+
import { InternalUserManagement } from "./internal_user_management";
|
|
13
14
|
/**
|
|
14
15
|
* Context that includes the internal controllers and contexts used by the app.
|
|
15
16
|
* Some controllers and context included in this context can be accessed
|
|
@@ -67,4 +68,19 @@ export type FireCMSContext<USER extends User = User, AuthControllerType extends
|
|
|
67
68
|
* Callback to send analytics events
|
|
68
69
|
*/
|
|
69
70
|
analyticsController?: AnalyticsController;
|
|
71
|
+
/**
|
|
72
|
+
* This section is used to manage users in the CMS.
|
|
73
|
+
* It is used to show user information in various places of the CMS,
|
|
74
|
+
* for example, to show who created or modified an entity,
|
|
75
|
+
* or to assign ownership of an entity.
|
|
76
|
+
*
|
|
77
|
+
* In the base CMS, this information is not used for access control.
|
|
78
|
+
* You can pass your own implementation of this section, to populate
|
|
79
|
+
* the dropdown of users when assigning ownership of an entity,
|
|
80
|
+
* or to show more information about the user.
|
|
81
|
+
*
|
|
82
|
+
* If you are using the FireCMS user management plugin, this
|
|
83
|
+
* section will be implemented automatically.
|
|
84
|
+
*/
|
|
85
|
+
userManagement: InternalUserManagement<USER>;
|
|
70
86
|
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from "./fields";
|
|
|
13
13
|
export * from "./property_config";
|
|
14
14
|
export * from "./datasource";
|
|
15
15
|
export * from "./entity_link_builder";
|
|
16
|
+
export * from "./internal_user_management";
|
|
16
17
|
export * from "./side_entity_controller";
|
|
17
18
|
export * from "./side_dialogs_controller";
|
|
18
19
|
export * from "./firecms_context";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { User } from "./user";
|
|
2
|
+
export type InternalUserManagement<USER extends User = User> = {
|
|
3
|
+
/**
|
|
4
|
+
* List of users to be managed by the CMS.
|
|
5
|
+
*/
|
|
6
|
+
users: USER[];
|
|
7
|
+
/**
|
|
8
|
+
* Function to get a user by its uid. This is used to show
|
|
9
|
+
* user information when assigning ownership of an entity.
|
|
10
|
+
*
|
|
11
|
+
* You can pass your own implementation if you want to show
|
|
12
|
+
* more information about the user.
|
|
13
|
+
*
|
|
14
|
+
* If you are using the FireCMS user management plugin, this
|
|
15
|
+
* function will be implemented automatically.
|
|
16
|
+
*
|
|
17
|
+
* @param uid
|
|
18
|
+
*/
|
|
19
|
+
getUser: (uid: string) => USER | null;
|
|
20
|
+
};
|
package/dist/types/plugins.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { CMSType, Property } from "./properties";
|
|
|
7
7
|
import { EntityStatus } from "./entities";
|
|
8
8
|
import { ResolvedProperty } from "./resolved_entities";
|
|
9
9
|
import { NavigationGroupMapping } from "./navigation";
|
|
10
|
+
import { InternalUserManagement } from "./internal_user_management";
|
|
10
11
|
/**
|
|
11
12
|
* Interface used to define plugins for FireCMS.
|
|
12
13
|
* NOTE: This is a work in progress and the API is not stable yet.
|
|
@@ -37,6 +38,7 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
37
38
|
}>>;
|
|
38
39
|
props?: PROPS;
|
|
39
40
|
};
|
|
41
|
+
userManagement?: InternalUserManagement;
|
|
40
42
|
homePage?: {
|
|
41
43
|
/**
|
|
42
44
|
* Additional actions to be rendered in the home page, close to the search bar.
|
|
@@ -291,6 +291,15 @@ export interface StringProperty extends BaseProperty<string> {
|
|
|
291
291
|
* indicate that this string refers to a path in your storage provider.
|
|
292
292
|
*/
|
|
293
293
|
storage?: StorageConfig;
|
|
294
|
+
/**
|
|
295
|
+
* This property is used to indicate that the string is a user ID, and
|
|
296
|
+
* it will be rendered as a user picker.
|
|
297
|
+
* Note that the user ID needs to be the one used in your authentication
|
|
298
|
+
* provider, e.g. Firebase Auth.
|
|
299
|
+
* You can also use a property builder to specify the user path dynamically
|
|
300
|
+
* based on other values of the entity.
|
|
301
|
+
*/
|
|
302
|
+
userSelect?: boolean;
|
|
294
303
|
/**
|
|
295
304
|
* If the value of this property is a URL, you can set this flag to true
|
|
296
305
|
* to add a link, or one of the supported media types to render a preview
|
|
@@ -37,4 +37,4 @@ export type PropertyConfig<T extends CMSType = any> = {
|
|
|
37
37
|
*/
|
|
38
38
|
description?: string;
|
|
39
39
|
};
|
|
40
|
-
export type PropertyConfigId = "text_field" | "multiline" | "markdown" | "url" | "email" | "select" | "multi_select" | "number_input" | "number_select" | "multi_number_select" | "file_upload" | "multi_file_upload" | "group" | "key_value" | "reference" | "reference_as_string" | "multi_references" | "switch" | "date_time" | "repeat" | "custom_array" | "block";
|
|
40
|
+
export type PropertyConfigId = "text_field" | "multiline" | "markdown" | "url" | "email" | "user_select" | "select" | "multi_select" | "number_input" | "number_select" | "multi_number_select" | "file_upload" | "multi_file_upload" | "group" | "key_value" | "reference" | "reference_as_string" | "multi_references" | "switch" | "date_time" | "repeat" | "custom_array" | "block";
|
package/dist/types/user.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firecms/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0.0-rc.
|
|
4
|
+
"version": "3.0.0-rc.2",
|
|
5
5
|
"description": "Awesome Firebase/Firestore-based headless open-source CMS",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/firecmsco"
|
|
@@ -53,9 +53,9 @@
|
|
|
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/editor": "^3.0.0-rc.
|
|
57
|
-
"@firecms/formex": "^3.0.0-rc.
|
|
58
|
-
"@firecms/ui": "^3.0.0-rc.
|
|
56
|
+
"@firecms/editor": "^3.0.0-rc.2",
|
|
57
|
+
"@firecms/formex": "^3.0.0-rc.2",
|
|
58
|
+
"@firecms/ui": "^3.0.0-rc.2",
|
|
59
59
|
"@radix-ui/react-portal": "^1.1.9",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
61
|
"date-fns": "^3.6.0",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"dist",
|
|
109
109
|
"src"
|
|
110
110
|
],
|
|
111
|
-
"gitHead": "
|
|
111
|
+
"gitHead": "6295de2d801a3d2cddaa958fd66cb041dd2c240e",
|
|
112
112
|
"publishConfig": {
|
|
113
113
|
"access": "public"
|
|
114
114
|
},
|
|
@@ -17,6 +17,7 @@ import { VirtualTableSelect } from "../VirtualTable/fields/VirtualTableSelect";
|
|
|
17
17
|
import { VirtualTableNumberInput } from "../VirtualTable/fields/VirtualTableNumberInput";
|
|
18
18
|
import { VirtualTableSwitch } from "../VirtualTable/fields/VirtualTableSwitch";
|
|
19
19
|
import { VirtualTableDateField } from "../VirtualTable/fields/VirtualTableDateField";
|
|
20
|
+
import { VirtualTableUserSelect } from "../VirtualTable/fields/VirtualTableUserSelect";
|
|
20
21
|
|
|
21
22
|
import { TableStorageUpload } from "./fields/TableStorageUpload";
|
|
22
23
|
import { TableReferenceField } from "./fields/TableReferenceField";
|
|
@@ -332,6 +333,17 @@ export const PropertyTableCell = React.memo<PropertyTableCellProps<any>>(
|
|
|
332
333
|
updateValue={updateValue}
|
|
333
334
|
/>;
|
|
334
335
|
fullHeight = true;
|
|
336
|
+
} else if (stringProperty.userSelect) {
|
|
337
|
+
innerComponent = <VirtualTableUserSelect name={propertyKey as string}
|
|
338
|
+
multiple={false}
|
|
339
|
+
focused={selected}
|
|
340
|
+
disabled={disabled}
|
|
341
|
+
small={getPreviewSizeFrom(size) !== "medium"}
|
|
342
|
+
error={validationError ?? error}
|
|
343
|
+
internalValue={internalValue as string}
|
|
344
|
+
updateValue={updateValue}
|
|
345
|
+
/>;
|
|
346
|
+
fullHeight = true;
|
|
335
347
|
} else if (stringProperty.markdown || !stringProperty.storage || !stringProperty.reference) {
|
|
336
348
|
const multiline = Boolean(stringProperty.multiline) || Boolean(stringProperty.markdown);
|
|
337
349
|
innerComponent = <VirtualTableInput error={validationError ?? error}
|
|
@@ -26,7 +26,7 @@ export function ErrorView({
|
|
|
26
26
|
tooltip
|
|
27
27
|
}: ErrorViewProps): React.ReactElement {
|
|
28
28
|
const component = error instanceof Error ? error.message : error;
|
|
29
|
-
|
|
29
|
+
console.warn("ErrorView", JSON.stringify(error))
|
|
30
30
|
|
|
31
31
|
const body = (
|
|
32
32
|
<div
|
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
|
2
2
|
import Fuse from "fuse.js";
|
|
3
3
|
import { Container, SearchBar } from "@firecms/ui";
|
|
4
4
|
import { useCustomizationController, useFireCMSContext, useNavigationController } from "../../hooks";
|
|
5
|
+
import { useCollapsedGroups } from "../../hooks/useCollapsedGroups";
|
|
5
6
|
import {
|
|
6
7
|
CMSAnalyticsEvent,
|
|
7
8
|
NavigationEntry,
|
|
@@ -194,14 +195,13 @@ export function DefaultHomePage({
|
|
|
194
195
|
onNavigationEntriesUpdate(all);
|
|
195
196
|
};
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
// Use custom hook for collapsed groups with localStorage persistence
|
|
199
|
+
const groupNames = useMemo(() => [
|
|
200
|
+
...items.map(item => item.name),
|
|
201
|
+
...(adminGroupData ? [adminGroupData.name] : [])
|
|
202
|
+
], [items, adminGroupData]);
|
|
201
203
|
|
|
202
|
-
const toggleGroupCollapsed =
|
|
203
|
-
setCollapsedGroups(prev => ({ ...prev, [name]: !prev[name] }));
|
|
204
|
-
}, []);
|
|
204
|
+
const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupNames);
|
|
205
205
|
|
|
206
206
|
|
|
207
207
|
const {
|
|
@@ -232,26 +232,10 @@ export function DefaultHomePage({
|
|
|
232
232
|
context.analyticsController?.onAnalyticsEvent?.("home_move_group", {
|
|
233
233
|
name: g
|
|
234
234
|
}),
|
|
235
|
-
onCardMovedBetweenGroups: (card) =>
|
|
236
|
-
// Find which group the card was moved to and expand it if collapsed
|
|
237
|
-
// Check both regular groups and admin group
|
|
238
|
-
let targetGroup = items.find(group =>
|
|
239
|
-
group.entries.some(entry => entry.url === card.url)
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
// Also check admin group if not found in regular groups
|
|
243
|
-
if (!targetGroup && adminGroupData?.entries.some(entry => entry.url === card.url)) {
|
|
244
|
-
targetGroup = adminGroupData;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (targetGroup && isGroupCollapsed(targetGroup.name)) {
|
|
248
|
-
toggleGroupCollapsed(targetGroup.name);
|
|
249
|
-
}
|
|
250
|
-
|
|
235
|
+
onCardMovedBetweenGroups: (card) =>
|
|
251
236
|
context.analyticsController?.onAnalyticsEvent?.("home_move_card", {
|
|
252
237
|
id: card.id
|
|
253
|
-
})
|
|
254
|
-
},
|
|
238
|
+
}),
|
|
255
239
|
onNewGroupDrop: () =>
|
|
256
240
|
context.analyticsController?.onAnalyticsEvent?.(
|
|
257
241
|
"home_drop_new_group"
|
|
@@ -576,7 +560,6 @@ export function DefaultHomePage({
|
|
|
576
560
|
onClose={() => setDialogOpenForGroup(null)}
|
|
577
561
|
onRename={(newName) => {
|
|
578
562
|
handleRenameGroup(dialogOpenForGroup, newName);
|
|
579
|
-
setDialogOpenForGroup(null);
|
|
580
563
|
}}
|
|
581
564
|
/>
|
|
582
565
|
)}
|
|
@@ -243,38 +243,11 @@ export function useHomePageDnd({
|
|
|
243
243
|
const collisionDetection: CollisionDetection = useCallback(
|
|
244
244
|
(args) => {
|
|
245
245
|
if (disabled || !activeId) return [];
|
|
246
|
-
|
|
247
246
|
if (activeIsGroup) {
|
|
248
247
|
const groups = args.droppableContainers.filter((c) =>
|
|
249
248
|
dndItems.some((g) => g.name === c.id)
|
|
250
249
|
);
|
|
251
250
|
if (!groups.length) return [];
|
|
252
|
-
|
|
253
|
-
// Special handling for dropping at the very beginning (first position)
|
|
254
|
-
if (groups.length > 0) {
|
|
255
|
-
const firstGroup = groups[0];
|
|
256
|
-
const firstGroupRect = firstGroup.rect.current;
|
|
257
|
-
const { x, y } = args.pointerCoordinates || { x: 0, y: 0 };
|
|
258
|
-
|
|
259
|
-
// If pointer is above the first group's top edge, treat it as dropping at position 0
|
|
260
|
-
if (firstGroupRect && y < firstGroupRect.top + 20) {
|
|
261
|
-
// Return the first group as target, but we'll handle this specially in onDragEnd
|
|
262
|
-
return [{ id: firstGroup.id, data: { insertBefore: true } }];
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Use closestCorners for better collision detection with collapsed groups
|
|
267
|
-
// This provides more precise drop zones between groups
|
|
268
|
-
const cornersResult = closestCorners({
|
|
269
|
-
...args,
|
|
270
|
-
droppableContainers: groups
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
if (cornersResult.length) {
|
|
274
|
-
return cornersResult;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Fallback to closestCenter if corners detection fails
|
|
278
251
|
return closestCenter({
|
|
279
252
|
...args,
|
|
280
253
|
droppableContainers: groups
|
|
@@ -407,21 +380,7 @@ export function useHomePageDnd({
|
|
|
407
380
|
|
|
408
381
|
/* ─── group reorder ─── */
|
|
409
382
|
if (activeIsGroup) {
|
|
410
|
-
|
|
411
|
-
const insertBefore = over.data?.current?.insertBefore;
|
|
412
|
-
|
|
413
|
-
if (insertBefore && activeIdNow !== overIdNow) {
|
|
414
|
-
// Move to first position (before the target group)
|
|
415
|
-
const from = dndItems.findIndex((g) => g.name === activeIdNow);
|
|
416
|
-
if (from !== -1 && from !== 0) {
|
|
417
|
-
const newState = arrayMove(dndItems, from, 0);
|
|
418
|
-
setDndItems(newState);
|
|
419
|
-
onPersist?.(newState);
|
|
420
|
-
onGroupMoved?.(activeIdNow as string, from, 0);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
// Handle dropping on another group (normal case)
|
|
424
|
-
else if (
|
|
383
|
+
if (
|
|
425
384
|
activeIdNow !== overIdNow &&
|
|
426
385
|
dndItems.some((g) => g.name === overIdNow)
|
|
427
386
|
) {
|
|
@@ -527,9 +486,7 @@ export function useHomePageDnd({
|
|
|
527
486
|
recentlyMovedToNewContainer.current = false;
|
|
528
487
|
};
|
|
529
488
|
|
|
530
|
-
const handleDragCancel = () =>
|
|
531
|
-
resetDragState();
|
|
532
|
-
};
|
|
489
|
+
const handleDragCancel = () => resetDragState();
|
|
533
490
|
|
|
534
491
|
/* ---------------- group rename ---------------- */
|
|
535
492
|
const handleRenameGroup = (oldName: string, newName: string) => {
|
|
@@ -547,6 +504,7 @@ export function useHomePageDnd({
|
|
|
547
504
|
onPersist?.(updated); // <- ensure rename is saved
|
|
548
505
|
return updated;
|
|
549
506
|
});
|
|
507
|
+
setDialogOpenForGroup(null);
|
|
550
508
|
};
|
|
551
509
|
|
|
552
510
|
/* ---------------- public API ---------------- */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField } from "@firecms/ui";
|
|
3
3
|
|
|
4
4
|
interface RenameGroupDialogProps {
|
|
@@ -9,7 +9,13 @@ interface RenameGroupDialogProps {
|
|
|
9
9
|
onRename: (newName: string) => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function RenameGroupDialog({
|
|
12
|
+
export function RenameGroupDialog({
|
|
13
|
+
open,
|
|
14
|
+
initialName,
|
|
15
|
+
existingGroupNames,
|
|
16
|
+
onClose,
|
|
17
|
+
onRename
|
|
18
|
+
}: RenameGroupDialogProps) {
|
|
13
19
|
const [name, setName] = useState(initialName);
|
|
14
20
|
const [error, setError] = useState<string | null>(null);
|
|
15
21
|
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement | null>(null); // Create a ref for the input
|
|
@@ -86,7 +92,7 @@ export function RenameGroupDialog({ open, initialName, existingGroupNames, onClo
|
|
|
86
92
|
if (!open) return null;
|
|
87
93
|
|
|
88
94
|
return (
|
|
89
|
-
<Dialog open={open}
|
|
95
|
+
<Dialog open={open}>
|
|
90
96
|
<DialogTitle>Rename Group</DialogTitle>
|
|
91
97
|
<DialogContent>
|
|
92
98
|
<TextField
|
|
@@ -9,7 +9,7 @@ export function PropertyConfigBadge({
|
|
|
9
9
|
propertyConfig: PropertyConfig | undefined,
|
|
10
10
|
disabled?: boolean
|
|
11
11
|
}): React.ReactNode {
|
|
12
|
-
const classes = "h-8 w-8
|
|
12
|
+
const classes = "h-8 w-8 flex items-center justify-center rounded-full shadow text-white " + (disabled ? "bg-surface-400 dark:bg-surface-600" : "");
|
|
13
13
|
|
|
14
14
|
const defaultPropertyConfig = typeof propertyConfig?.property === "object" ? getDefaultFieldConfig(propertyConfig.property) : undefined;
|
|
15
15
|
|
|
@@ -18,6 +18,6 @@ export function PropertyConfigBadge({
|
|
|
18
18
|
style={{
|
|
19
19
|
background: !disabled ? (propertyConfig?.color ?? defaultPropertyConfig?.color ?? "#888") : undefined
|
|
20
20
|
}}>
|
|
21
|
-
{propertyConfig?.Icon ? getIconForWidget(propertyConfig, "
|
|
21
|
+
{propertyConfig?.Icon ? getIconForWidget(propertyConfig, "small") : getIconForWidget(defaultPropertyConfig, "small")}
|
|
22
22
|
</div>
|
|
23
23
|
}
|
|
@@ -123,7 +123,7 @@ export function StringNumberFilterField({
|
|
|
123
123
|
: evt.target.value;
|
|
124
124
|
updateFilter(operation, val);
|
|
125
125
|
}}
|
|
126
|
-
endAdornment={internalValue && <IconButton
|
|
126
|
+
endAdornment={internalValue !== undefined && internalValue != null && <IconButton
|
|
127
127
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
128
128
|
<CloseIcon/>
|
|
129
129
|
</IconButton>}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { User } from "../types";
|
|
2
|
+
import { AccountCircleIcon, cls, defaultBorderMixin } from "@firecms/ui";
|
|
3
|
+
import { EmptyValue } from "../preview";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Component to render a single user with name and email
|
|
7
|
+
*/
|
|
8
|
+
export function UserDisplay({
|
|
9
|
+
user,
|
|
10
|
+
}: { user: User | null }) {
|
|
11
|
+
if (!user) {
|
|
12
|
+
return <EmptyValue/>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const avatarSizeClass = "w-6 h-6";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={cls(
|
|
19
|
+
"inline-flex items-center gap-4 px-2 py-1 rounded-xl",
|
|
20
|
+
"bg-surface-accent-100 dark:bg-surface-accent-800",
|
|
21
|
+
"border",
|
|
22
|
+
defaultBorderMixin
|
|
23
|
+
)}>
|
|
24
|
+
{user.photoURL ? (
|
|
25
|
+
<img
|
|
26
|
+
src={user.photoURL}
|
|
27
|
+
alt={user.displayName || user.email || "User"}
|
|
28
|
+
className={cls(
|
|
29
|
+
"rounded-full object-cover",
|
|
30
|
+
avatarSizeClass
|
|
31
|
+
)}
|
|
32
|
+
/>
|
|
33
|
+
) : (
|
|
34
|
+
<AccountCircleIcon
|
|
35
|
+
className={cls(
|
|
36
|
+
"text-text-secondary dark:text-text-secondary-dark",
|
|
37
|
+
avatarSizeClass
|
|
38
|
+
)}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
<div className="flex flex-col min-w-0">
|
|
42
|
+
<span className={cls("font-regular truncate", "text-sm")}>
|
|
43
|
+
{user.displayName || user.email || "-"}
|
|
44
|
+
</span>
|
|
45
|
+
{user.displayName && user.email && (
|
|
46
|
+
<span className={cls("text-text-secondary dark:text-text-secondary-dark truncate",
|
|
47
|
+
"text-xs"
|
|
48
|
+
)}>
|
|
49
|
+
{user.email}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useCallback, useEffect } from "react";
|
|
2
|
+
import { MultiSelect, MultiSelectItem, Select, SelectItem } from "@firecms/ui";
|
|
3
|
+
import { useInternalUserManagementController } from "../../../hooks";
|
|
4
|
+
import { UserDisplay } from "../../UserDisplay";
|
|
5
|
+
|
|
6
|
+
export function VirtualTableUserSelect(props: {
|
|
7
|
+
name: string;
|
|
8
|
+
error: Error | undefined;
|
|
9
|
+
multiple: boolean;
|
|
10
|
+
disabled: boolean;
|
|
11
|
+
small: boolean;
|
|
12
|
+
internalValue: string | string[] | undefined;
|
|
13
|
+
updateValue: (newValue: (string | string[] | null)) => void;
|
|
14
|
+
focused: boolean;
|
|
15
|
+
onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
|
16
|
+
}) {
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
internalValue,
|
|
20
|
+
disabled,
|
|
21
|
+
small,
|
|
22
|
+
focused,
|
|
23
|
+
updateValue,
|
|
24
|
+
multiple
|
|
25
|
+
} = props;
|
|
26
|
+
|
|
27
|
+
const { users, getUser } = useInternalUserManagementController();
|
|
28
|
+
|
|
29
|
+
const validValue = (Array.isArray(internalValue) && multiple) ||
|
|
30
|
+
(!Array.isArray(internalValue) && !multiple);
|
|
31
|
+
|
|
32
|
+
const ref = React.useRef<HTMLButtonElement>(null);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (ref.current && focused) {
|
|
35
|
+
ref.current?.focus({ preventScroll: true });
|
|
36
|
+
}
|
|
37
|
+
}, [focused, ref]);
|
|
38
|
+
|
|
39
|
+
const onChange = useCallback((updatedValue: string | string[]) => {
|
|
40
|
+
if (!updatedValue) {
|
|
41
|
+
updateValue(null);
|
|
42
|
+
} else {
|
|
43
|
+
updateValue(updatedValue);
|
|
44
|
+
}
|
|
45
|
+
}, [updateValue]);
|
|
46
|
+
|
|
47
|
+
const renderValue = (userId: string) => {
|
|
48
|
+
const user = getUser(userId);
|
|
49
|
+
return <UserDisplay user={user} />;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
multiple
|
|
54
|
+
? <MultiSelect
|
|
55
|
+
inputRef={ref}
|
|
56
|
+
className="w-full h-full p-0 bg-transparent"
|
|
57
|
+
position={"item-aligned"}
|
|
58
|
+
disabled={disabled}
|
|
59
|
+
includeClear={false}
|
|
60
|
+
useChips={false}
|
|
61
|
+
value={validValue
|
|
62
|
+
? (internalValue as string[])
|
|
63
|
+
: ([])}
|
|
64
|
+
onValueChange={onChange}>
|
|
65
|
+
{users?.map((user) => (
|
|
66
|
+
<MultiSelectItem
|
|
67
|
+
key={user.uid}
|
|
68
|
+
value={user.uid}>
|
|
69
|
+
<UserDisplay
|
|
70
|
+
user={user} />
|
|
71
|
+
</MultiSelectItem>
|
|
72
|
+
))}
|
|
73
|
+
</MultiSelect>
|
|
74
|
+
: <Select
|
|
75
|
+
inputRef={ref}
|
|
76
|
+
size={"large"}
|
|
77
|
+
fullWidth={true}
|
|
78
|
+
className="w-full h-full p-0 bg-transparent"
|
|
79
|
+
position={"item-aligned"}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
padding={false}
|
|
82
|
+
value={validValue
|
|
83
|
+
? internalValue as string
|
|
84
|
+
: ""}
|
|
85
|
+
onValueChange={onChange}
|
|
86
|
+
renderValue={renderValue}>
|
|
87
|
+
{users?.map((user) => (
|
|
88
|
+
<SelectItem
|
|
89
|
+
key={user.uid}
|
|
90
|
+
value={user.uid}>
|
|
91
|
+
<UserDisplay
|
|
92
|
+
user={user}/>
|
|
93
|
+
</SelectItem>
|
|
94
|
+
))}
|
|
95
|
+
</Select>
|
|
96
|
+
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
@@ -8,14 +8,7 @@ export const COLLECTION_GROUP_PARENT_ID = "collectionGroupParent";
|
|
|
8
8
|
export function useColumnIds<M extends Record<string, any>>(collection: ResolvedEntityCollection<M>, includeSubcollections: boolean): PropertyColumnConfig[] {
|
|
9
9
|
return useMemo(() => {
|
|
10
10
|
if (collection.propertiesOrder) {
|
|
11
|
-
|
|
12
|
-
if (collection.collectionGroup) {
|
|
13
|
-
propertyColumnConfigs.push({
|
|
14
|
-
key: COLLECTION_GROUP_PARENT_ID,
|
|
15
|
-
disabled: true
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
return propertyColumnConfigs;
|
|
11
|
+
return hideAndExpandKeys(collection, collection.propertiesOrder);
|
|
19
12
|
}
|
|
20
13
|
return getDefaultColumnKeys(collection, includeSubcollections);
|
|
21
14
|
}, [collection, includeSubcollections]);
|