@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.
Files changed (63) hide show
  1. package/dist/components/UserDisplay.d.ts +7 -0
  2. package/dist/components/VirtualTable/fields/VirtualTableUserSelect.d.ts +12 -0
  3. package/dist/contexts/InternalUserManagementContext.d.ts +3 -0
  4. package/dist/core/FireCMS.d.ts +0 -1
  5. package/dist/core/field_configs.d.ts +1 -1
  6. package/dist/form/field_bindings/UserSelectFieldBinding.d.ts +12 -0
  7. package/dist/hooks/index.d.ts +2 -0
  8. package/dist/hooks/useCollapsedGroups.d.ts +9 -0
  9. package/dist/hooks/useInternalUserManagementController.d.ts +12 -0
  10. package/dist/index.es.js +617 -208
  11. package/dist/index.es.js.map +1 -1
  12. package/dist/index.umd.js +615 -206
  13. package/dist/index.umd.js.map +1 -1
  14. package/dist/preview/components/UserPreview.d.ts +8 -0
  15. package/dist/preview/index.d.ts +1 -0
  16. package/dist/types/collections.d.ts +2 -0
  17. package/dist/types/entities.d.ts +5 -1
  18. package/dist/types/firecms.d.ts +15 -0
  19. package/dist/types/firecms_context.d.ts +16 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/types/internal_user_management.d.ts +20 -0
  22. package/dist/types/plugins.d.ts +2 -0
  23. package/dist/types/properties.d.ts +9 -0
  24. package/dist/types/property_config.d.ts +1 -1
  25. package/dist/types/user.d.ts +1 -1
  26. package/package.json +5 -5
  27. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +12 -0
  28. package/src/components/ErrorView.tsx +1 -1
  29. package/src/components/HomePage/DefaultHomePage.tsx +9 -26
  30. package/src/components/HomePage/HomePageDnD.tsx +3 -45
  31. package/src/components/HomePage/RenameGroupDialog.tsx +9 -3
  32. package/src/components/PropertyConfigBadge.tsx +2 -2
  33. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +1 -1
  34. package/src/components/UserDisplay.tsx +55 -0
  35. package/src/components/VirtualTable/fields/VirtualTableUserSelect.tsx +99 -0
  36. package/src/components/common/useColumnsIds.tsx +1 -8
  37. package/src/contexts/InternalUserManagementContext.tsx +4 -0
  38. package/src/core/FireCMS.tsx +22 -13
  39. package/src/core/field_configs.tsx +15 -1
  40. package/src/form/PropertyFieldBinding.tsx +4 -0
  41. package/src/form/field_bindings/UserSelectFieldBinding.tsx +94 -0
  42. package/src/hooks/index.tsx +3 -0
  43. package/src/hooks/useBrowserTitleAndIcon.tsx +1 -1
  44. package/src/hooks/useCollapsedGroups.ts +64 -0
  45. package/src/hooks/useFireCMSContext.tsx +6 -2
  46. package/src/hooks/useInternalUserManagementController.tsx +16 -0
  47. package/src/preview/PropertyPreview.tsx +8 -0
  48. package/src/preview/components/ReferencePreview.tsx +4 -2
  49. package/src/preview/components/UserPreview.tsx +27 -0
  50. package/src/preview/index.ts +1 -0
  51. package/src/preview/property_previews/ArrayPropertyPreview.tsx +1 -1
  52. package/src/types/collections.ts +2 -0
  53. package/src/types/entities.ts +7 -1
  54. package/src/types/firecms.tsx +16 -0
  55. package/src/types/firecms_context.tsx +17 -0
  56. package/src/types/index.ts +1 -0
  57. package/src/types/internal_user_management.ts +24 -0
  58. package/src/types/plugins.tsx +3 -0
  59. package/src/types/properties.ts +10 -0
  60. package/src/types/property_config.tsx +1 -0
  61. package/src/types/user.ts +1 -1
  62. package/src/util/entities.ts +1 -1
  63. 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;
@@ -21,3 +21,4 @@ export * from "./components/EnumValuesChip";
21
21
  export * from "./components/EmptyValue";
22
22
  export * from "./components/ImagePreview";
23
23
  export * from "./components/ReferencePreview";
24
+ export * from "./components/UserPreview";
@@ -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
  /**
@@ -42,7 +42,11 @@ export declare class EntityReference {
42
42
  * to the root of the database).
43
43
  */
44
44
  readonly path: string;
45
- constructor(id: string, path: string);
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
  }
@@ -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
  };
@@ -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
+ };
@@ -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";
@@ -35,7 +35,7 @@ export type User = {
35
35
  */
36
36
  readonly isAnonymous: boolean;
37
37
  /**
38
- *
38
+ * Custom roles assigned to the user.
39
39
  */
40
40
  roles?: Role[];
41
41
  getIdToken?: (forceRefresh?: boolean) => Promise<string>;
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.1",
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.1",
57
- "@firecms/formex": "^3.0.0-rc.1",
58
- "@firecms/ui": "^3.0.0-rc.1",
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": "e91ae67d24262a9b944550abaae626f182688aa1",
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
- // console.warn("ErrorView", error)
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
- const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
198
- const isGroupCollapsed = useCallback((name: string) => {
199
- return !!collapsedGroups[name];
200
- }, [collapsedGroups]);
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 = useCallback((name: string) => {
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
- // Check if we're dropping above the first group (insertBefore flag)
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, { useState, useEffect, useRef } from "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({ open, initialName, existingGroupNames, onClose, onRename }: RenameGroupDialogProps) {
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} onOpenChange={onClose}>
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 p-1 rounded-full shadow text-white " + (disabled ? "bg-surface-400 dark:bg-surface-600" : "");
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, "medium") : getIconForWidget(defaultPropertyConfig, "medium")}
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
- const propertyColumnConfigs = hideAndExpandKeys(collection, collection.propertiesOrder);
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]);
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import { InternalUserManagement, NavigationController } from "../types";
3
+
4
+ export const InternalUserManagementContext = React.createContext<InternalUserManagement<any>>({} as InternalUserManagement);