@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
@@ -20,6 +20,7 @@ import { CustomizationControllerContext } from "../contexts/CustomizationControl
20
20
  import { AnalyticsContext } from "../contexts/AnalyticsContext";
21
21
  import { useProjectLog } from "../hooks/useProjectLog";
22
22
  import { BreadcrumbsProvider } from "../contexts/BreacrumbsContext";
23
+ import { InternalUserManagementContext } from "../contexts/InternalUserManagementContext";
23
24
 
24
25
  /**
25
26
  * If you are using independent components of the CMS
@@ -30,7 +31,6 @@ import { BreadcrumbsProvider } from "../contexts/BreacrumbsContext";
30
31
  *
31
32
  * You only need to use this component if you are building a custom app.
32
33
  *
33
-
34
34
  * @group Core
35
35
  */
36
36
  export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
@@ -44,21 +44,28 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
44
44
  authController,
45
45
  storageSource,
46
46
  dataSourceDelegate,
47
- plugins: pluginsProp,
47
+ plugins: _pluginsProp,
48
48
  onAnalyticsEvent,
49
49
  propertyConfigs,
50
50
  entityViews,
51
51
  entityActions,
52
52
  components,
53
53
  navigationController,
54
- apiKey
54
+ apiKey,
55
+ userManagement: _userManagement
55
56
  } = props;
56
57
 
57
- if (pluginsProp) {
58
+ if (_pluginsProp) {
58
59
  console.warn("The `plugins` prop is deprecated in the FireCMS component. You should pass your plugins to `useBuildNavigationController` instead.");
59
60
  }
60
61
 
61
- const plugins = navigationController.plugins ?? pluginsProp;
62
+ const plugins = navigationController.plugins ?? _pluginsProp;
63
+ const userManagement = plugins?.find(p => p.userManagement)?.userManagement
64
+ ?? _userManagement
65
+ ?? {
66
+ users: [],
67
+ getUser: (uid: string) => null
68
+ };
62
69
 
63
70
  const sideDialogsController = useBuildSideDialogsController();
64
71
  const sideEntityController = useBuildSideEntityController(navigationController, sideDialogsController, authController);
@@ -156,14 +163,16 @@ export function FireCMS<USER extends User>(props: FireCMSProps<USER>) {
156
163
  value={sideEntityController}>
157
164
  <NavigationContext.Provider
158
165
  value={navigationController}>
159
- <DialogsProvider>
160
- <BreadcrumbsProvider>
161
- <FireCMSInternal
162
- loading={loading}>
163
- {children}
164
- </FireCMSInternal>
165
- </BreadcrumbsProvider>
166
- </DialogsProvider>
166
+ <InternalUserManagementContext.Provider value={userManagement}>
167
+ <DialogsProvider>
168
+ <BreadcrumbsProvider>
169
+ <FireCMSInternal
170
+ loading={loading}>
171
+ {children}
172
+ </FireCMSInternal>
173
+ </BreadcrumbsProvider>
174
+ </DialogsProvider>
175
+ </InternalUserManagementContext.Provider>
167
176
  </NavigationContext.Provider>
168
177
  </SideEntityControllerContext.Provider>
169
178
  </SideDialogsControllerContext.Provider>
@@ -32,14 +32,16 @@ import {
32
32
  ListAltIcon,
33
33
  ListIcon,
34
34
  MailIcon,
35
- NumbersIcon,
35
+ NumbersIcon, PersonIcon,
36
36
  RepeatIcon,
37
37
  ScheduleIcon,
38
38
  ShortTextIcon,
39
39
  SubjectIcon,
40
40
  UploadFileIcon,
41
+ VerifiedUserIcon,
41
42
  ViewStreamIcon
42
43
  } from "@firecms/ui";
44
+ import { UserSelectFieldBinding } from "../form/field_bindings/UserSelectFieldBinding";
43
45
 
44
46
  export function isDefaultFieldConfigId(id: string) {
45
47
  return Object.keys(DEFAULT_FIELD_CONFIGS).includes(id);
@@ -143,6 +145,16 @@ export const DEFAULT_FIELD_CONFIGS: Record<string, PropertyConfig<any>> = {
143
145
  Field: MultiSelectFieldBinding
144
146
  }
145
147
  },
148
+ user_select: {
149
+ key: "user_select",
150
+ name: "User select",
151
+ description: "Select a user from the user management system. Store the user ID.",
152
+ Icon: PersonIcon,
153
+ property: {
154
+ dataType: "string",
155
+ Field: UserSelectFieldBinding
156
+ }
157
+ },
146
158
  number_input: {
147
159
  key: "number_input",
148
160
  name: "Number input",
@@ -360,6 +372,8 @@ export function getDefaultFieldId(property: Property | ResolvedProperty) {
360
372
  return "email";
361
373
  } else if (property.enumValues) {
362
374
  return "select";
375
+ } else if (property.userSelect) {
376
+ return "user_select";
363
377
  } else if (property.reference) {
364
378
  return "reference_as_string";
365
379
  } else {
@@ -93,6 +93,10 @@ function PropertyFieldBindingInternal<T extends CMSType = CMSType, M extends Rec
93
93
  const authController = useAuthController();
94
94
  const customizationController = useCustomizationController();
95
95
 
96
+ if(propertyKey === "created_by"){
97
+ console.log("Rendering field for created_by", {propertyKey, property, context});
98
+ }
99
+
96
100
  return (
97
101
  <Field
98
102
  key={propertyKey}
@@ -0,0 +1,94 @@
1
+ import React, { useCallback } from "react";
2
+
3
+ import { FieldProps, User } from "../../types";
4
+ import { FieldHelperText, LabelWithIcon } from "../components";
5
+ import { getIconForProperty } from "../../util";
6
+ import { CloseIcon, cls, IconButton, Select, SelectItem } from "@firecms/ui";
7
+ import { PropertyIdCopyTooltip } from "../../components";
8
+ import { useInternalUserManagementController } from "../../hooks";
9
+ import { UserDisplay } from "../../components/UserDisplay";
10
+
11
+ type UserSelectProps = FieldProps<string>;
12
+
13
+ /**
14
+ * Field binding for selecting a user from the internal user management system.
15
+ * Renders a select dropdown with user information including name and email.
16
+ *
17
+ * This is one of the internal components that get mapped natively inside forms
18
+ * and tables to the specified properties.
19
+ * @group Form fields
20
+ */
21
+ export function UserSelectFieldBinding({
22
+ propertyKey,
23
+ value,
24
+ setValue,
25
+ error,
26
+ showError,
27
+ disabled,
28
+ autoFocus,
29
+ touched,
30
+ property,
31
+ includeDescription,
32
+ size = "large"
33
+ }: UserSelectProps) {
34
+
35
+ const { users, getUser } = useInternalUserManagementController();
36
+
37
+ const handleClearClick = useCallback((e: React.MouseEvent) => {
38
+ e.stopPropagation();
39
+ e.preventDefault();
40
+ setValue(null);
41
+ }, [setValue]);
42
+
43
+ return (
44
+ <>
45
+ <Select
46
+ value={value !== undefined && value != null ? value.toString() : ""}
47
+ disabled={disabled}
48
+ size={size}
49
+ fullWidth={true}
50
+ position="item-aligned"
51
+ inputClassName={cls("w-full")}
52
+ label={
53
+ <PropertyIdCopyTooltip propertyKey={propertyKey}>
54
+ <LabelWithIcon
55
+ icon={getIconForProperty(property, "small")}
56
+ required={property.validation?.required}
57
+ title={property.name}
58
+ className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5 my-0"}
59
+ />
60
+ </PropertyIdCopyTooltip>}
61
+ endAdornment={
62
+ property.clearable && !disabled && value && <IconButton
63
+ size="small"
64
+ onClick={handleClearClick}>
65
+ <CloseIcon size={"small"}/>
66
+ </IconButton>
67
+ }
68
+ onValueChange={(updatedValue: string) => {
69
+ const newValue = updatedValue || null;
70
+ return setValue(newValue);
71
+ }}
72
+ renderValue={(userId: string) => {
73
+ const user = getUser(userId);
74
+ return <UserDisplay user={user} />;
75
+ }}
76
+ >
77
+ {users && users.map((user) => {
78
+ return <SelectItem
79
+ key={user.uid}
80
+ value={user.uid}>
81
+ <UserDisplay user={user} />
82
+ </SelectItem>
83
+ })}
84
+ </Select>
85
+
86
+ <FieldHelperText includeDescription={includeDescription}
87
+ showError={showError}
88
+ error={error}
89
+ disabled={disabled}
90
+ property={property}/>
91
+
92
+ </>
93
+ );
94
+ }
@@ -18,6 +18,9 @@ export * from "./useSnackbarController";
18
18
  export * from "./useModeController";
19
19
  export * from "./useClipboard";
20
20
  export * from "./useLargeLayout";
21
+ export * from "./useCollapsedGroups";
22
+
23
+ export * from "./useInternalUserManagementController";
21
24
 
22
25
  export * from "./useReferenceDialog";
23
26
  export * from "./useBrowserTitleAndIcon";
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from "react";
2
2
 
3
- const fireCMSLogo = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAuDSURBVHgB7Z1fTFvXHcd/GDAk2RaHpmmTjMUVaE3bEJyHRO221OYp6VQJ8zaqSpg+beokHKnStIcKo0rrSyXgIdWeipFa8YjR+tA8YbKHTclDvJCp05QoZGuTaVsTp9vCf9j9XueCjX3te+17f+dc+3yki7EhIvbve35/zu+cc5u2NajOWHq4RenbW/R4eZsy97cou5x7Lbu8/fTnxW85sK9Ju4zH3BXsIAoe8lHvcR+FtAuv1RtNXheAYeyFO5uU+XpLv9wCAgjpYmiicFczRbqbPS8KzwkAozi1uKkbHI/GqBYFBBHp9lH/qRZdEF7DEwIwjD59fUMf4aKNbkawo0kXwdBZ74hBagGkb2/S3K0NSl4TP9LtAjEkLvgprHmHYIePZEVKAcDwY1fW9cd6IHauhUYvtEopBKkEkLy2oRl+rWSWXg/IKAQpBFDvht+LTEIQKoB6c/V2kUEIQgSAhA6Gn1hYp0bHSBaHNDGIgF0AqcUNGp5Z81xW7zYoG6cG/ezegE0AMDgMDwEoSoNZRXiDkTCfN2ARAGL88MxqwyR5tcKZG7gugMmrGxSfXSWFPZAbzL/b7roIXBMAXP6l1Jpe4imqZyLa5mpIcEUAcPUDn6y42plrJBIX/XpIcAPHBQDj911eVvHeYeLhVhqP+slpHBWAMr67IDmcGmwjJ3FMAMr4PER7WvT5AqcWojiSYirj84F5FCTXTlGzAJTx+UFlhXkVJ6hJAMr44oAInPAEVQsAdT5KPWV8caCZhqZaLVQtAPxhVeeLJ/FFbf2VqgSgWrlygSYblsdXg+0yEKP+zEfL5FUCLVkKND/Ofa894lpa+4H+fGn1BHkVLE9H78BueWhLAF5K+mDo0L5F7bpJvQdu6t8H/X/TXy9HdiNAmeUeym4epIVvz2vfn6b0f86TF6hmttCWAFB6yNzcgXFjHZ9R/6HPdYNXMrZVIIr0f39Cc4/e1B7PS+0p4AXs7EmwLAAna0+niQY+p5HnLlPku78nDlLZN2n632/rj7KBNvKN9/ZZDgWWBCCj68foHjnyMcW1y6mRbhd4grH7v6bkN2+TTNgJBZYEIJvrx2hPHP1QmOH3IqMQrIaCigLAqH/hgyckA3DxU8GfU7DtHslI5slpGrgzI0WOgFBw9/39FX+v4jwAXL9oMNLHO39F8y++Ia3xQWj/Tbrb8wqNHvsNiQYDd3Khstcu6wFkSPxg8Pkf/lRqw5cCpePw0m+FegMkgnffL58QlvUA2K4lkmjgd3TjpR97zvgA4Uq0cNGvmawwY2sqAIx+kVk/Er3Z7kFpEr1qgPFvvPwjvUwVxcTVjbKbcEwFIHL0I4ZOaDG/HsBU82z3z2jo8KckgkpeoKQARI5+GD8hQRLlNEmtehElgnJeoKQAcBSLCOrV+AYQAddsZT4w/rTJPE6RANDtE7FdGx9MPRvfAOFARGKYulXapkUCmLzK3+fHB4IJnkYAOQGqA6MlzQUGdamBXSQAnMbFjRfr/FrAex0XkOTOlVg5VCAAJH/c+/YR9xvJ+AYxLSGMa6UuJ8nrxaetFQgAR7JxAsM3Qtw3A+LnDAUw/t51nL78H3K7/9muQWpkYHzuvsHeamBHAAvMmX/smU/15kmjE2dcyAL2VgM+sx+4zeixD0mRg9MLwNPnVwM7AuCs/TH6GzHxMwMegNML5FcDugCQGHBO/Y489zEpChlhrAgy93dtvSMALqB0FfuLQceQqyKAtzfKQV0AOHufi6HDn5GiNJzzAsagf+oB+AQQ+Q5/M8QrhBnzgD8VCoAn/sP9q+TPHM7PZ8cDcMZ/5f4rw7V6CDfTAr57D/kEgH16ivJwlYNG1cfmAZDhquy/Mlx5AKoAbCn3cXX/lPGtgYHClQfgfoq+pUdKALLBFSpRCfiyTBt/sDdfYY1gG99n5VtiSgJ7lQewDNdg0XMAYoJ7DZyXOci0GQb5n4+rCaQEYB0uD6AngcSEl7d41TNsN6cJ/LVT+9pJCgs08X1ObALIpvspsLFCCgscDBK9TCywhYBsSzsp5MPn1LnzCm+iCYBYWPIHSGENrs8K5wixhYB77UoAVuH8rHxctyrNNqscwCqZA88TB7C9L3iIJwfIHDhKCmtkm3ni8gmEAK4cIP29ICmskdnP4wFQALCFgKW2gCoFLQD3z/U56UkgvnDBpWwvs8DkKTH6dQ/Qe9z60eK1MvfMSVKUJ9XxEnEQOpbz/LoH4JoMSh4+Qwpz4Pq5cqXQ8ZzNfbknTKUg4xv0IqlDfB4y/PQk8ZwAjvHlASoMmDN9hM9DGsm//tXOLUZqBWFAVQPFoEpKMyaAhtfXv4YZBQDjTx59lRSFjHVGiItI1669dQFAEaxe4FmVDOaD0c/5mUS6d3O+ne8iXWx9If0NKy+wC+foB/ke31fqRQ4S3+9TuQDxj36U/flV364H0ATAuThE5QI5uEd/9FThQC/w+7Gz/F6Aq/UpI8lnQ+z50NC51oLnBQLo72FbI7rDcPcANSJw/WOdfcTJXvcPCp4hDBhThFygQXQp+AY1GnD9EAEn8ddbi14rSv2jp/i9wISWC3DOgokGI19EKdzfUxziiwQwEm4lESuF4ycuNkQ+kOo4qeU+EeImdraFSq39KHoFxudOBgGqgoEXB9ndIicId8NdYnKeoXOlPXvJ2R8RySCA8fteGa5LEeA9DZwcFDL3geTPbKa3pADwy5Fufi8A6lEEGPlnTv9C2HsavWB+J3Gf+T9qJVHUkwgQ8/FeRM16YvTHzpl7dFMBiPQCAMbHqJk+EiKvMnn0NT2vETnlXW70g7I3j8YRcmc+En/38MRXaRr9+zx5BRgcyR5Gv0is3EK+rADApdlV/c6TogmuZmn+z1P6o8ykDwZ148sQvm68t6/icr+KAsA5Mi98sMx+NzEz4g/+SCMP/iCdEGBwzGiKHvUGqPun3mqr+HsVBQAmFtbpUkrsreTzgfETX83T0D8zJJpcV/M1mnj+VWna23D98++2k5VNP5YEAPourwi5pWw5IAR4hP6HX7J7BBkNbzA12FY288/HsgBwmhgSQllCwV5i/8pQ/6MvKfrNX8gtYGi4+GltHl/W5e1Y7zf/S+uCtCwAIFsoKAXOIYp8u0RRTQy9//sHhbSrFtCfwHYt7NjBhI7Mq5jsuH4DWwIAA5+sUmpRfFVgFQgi9EQTwpMHFFzJ0sHNlZLhAobFGQaPtQtb2WF4nNThpWVrdly/gW0BIAQgFHDeZUxRGczcJi76bf8720uB0S2cfaed1OFS8oBavxrjg6rWguMPjker+4MKZ0Hcn32ncr1vRtWbARBrRDaMFDlvbDfp20tNu0HgduKvi1k7oEDS56daT3ipeTvQ+ECb6WoThXsg4486sHDHkf1gSe0/I7J13Ggg9MYcGnSObQhEIqI8gfsg+a424y+F7XmASsRmVmn6mncmirxENRM9lXBcACDxxRqNXVknhTPk5l7cCbOu+GzDRSkR1E6uzm937RwnVzyAAXoGwzNr0nYQZQdGx8h38zBPVwUA0DPou6x6B3YZ0eZXJgaqn+GziusCMJBlbaHsIN5jgifKtDmHTQAgqVUHY1fWlDcwAYs5pt7yE9f5zYBVAADGH9OqhOR15Q0MMOoxuRMP8/dW2AVggAQRq4sa3Rtg9e74gF9Ye12YAAwaNSzA3Y9ebBU+hS5cAADGT15bp2ktLNS7EGQxvIEUAjCoZyHIZngDqQSQT72EBlkNbyCtAAywGQXNpdStTc/MKOJGXOiMxgUdt2MH6QWQDyqHucVNKcUAo0d7mvXTVby0NsJTAsgHnmFOE0Tm621K3+HfsqYfsK2593C3Tzc85+SNk3hWAPnAG+AsA1wLt7e0vEG7Hm075iUwumFgnKGIeyzhtG2vGnwvdSEAMwxh4BHJ5OOngjBLLI07qJ3QjIv7KaIbZ9xdq175P/vCeAzHXKL0AAAAAElFTkSuQmCC";
3
+ const fireCMSLogo = "data:image/png;base64,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";
4
4
 
5
5
  /**
6
6
  * Internal hook to handle the browser title and icon
@@ -0,0 +1,64 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ /**
4
+ * Custom hook for managing collapsed/expanded state of navigation groups
5
+ * with localStorage persistence. Automatically cleans up stale group entries
6
+ * when groups are removed from the navigation.
7
+ */
8
+ export function useCollapsedGroups(groupNames: string[]) {
9
+ // Load collapsed groups from localStorage on mount
10
+ const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
11
+ try {
12
+ const stored = localStorage.getItem('firecms-collapsed-groups');
13
+ return stored ? JSON.parse(stored) : {};
14
+ } catch {
15
+ return {};
16
+ }
17
+ });
18
+
19
+ // Save to localStorage whenever collapsedGroups changes
20
+ useEffect(() => {
21
+ try {
22
+ localStorage.setItem('firecms-collapsed-groups', JSON.stringify(collapsedGroups));
23
+ } catch {
24
+ // Silently fail if localStorage is not available
25
+ }
26
+ }, [collapsedGroups]);
27
+
28
+ // Clean up collapsed groups state when groups change - remove entries for groups that no longer exist
29
+ useEffect(() => {
30
+ // Only clean up if we have actual groups loaded (avoid cleaning up during initial load)
31
+ if (groupNames.length === 0) return;
32
+
33
+ const currentGroupNames = new Set(groupNames);
34
+
35
+ setCollapsedGroups(prev => {
36
+ const cleaned = Object.fromEntries(
37
+ Object.entries(prev).filter(([groupName]) => currentGroupNames.has(groupName))
38
+ );
39
+
40
+ // Only update if something actually changed
41
+ const prevKeys = Object.keys(prev);
42
+ const cleanedKeys = Object.keys(cleaned);
43
+
44
+ if (prevKeys.length === cleanedKeys.length && prevKeys.every(key => cleanedKeys.includes(key))) {
45
+ return prev;
46
+ }
47
+
48
+ return cleaned;
49
+ });
50
+ }, [groupNames]);
51
+
52
+ const isGroupCollapsed = useCallback((name: string) => {
53
+ return !!collapsedGroups[name];
54
+ }, [collapsedGroups]);
55
+
56
+ const toggleGroupCollapsed = useCallback((name: string) => {
57
+ setCollapsedGroups(prev => ({ ...prev, [name]: !prev[name] }));
58
+ }, []);
59
+
60
+ return {
61
+ isGroupCollapsed,
62
+ toggleGroupCollapsed
63
+ };
64
+ }
@@ -11,6 +11,7 @@ import { useDialogsController } from "./useDialogsController";
11
11
  import { useCustomizationController } from "./useCustomizationController";
12
12
  import { useAnalyticsController } from "./useAnalyticsController";
13
13
  import React, { useEffect } from "react";
14
+ import { useInternalUserManagementController } from "./useInternalUserManagementController";
14
15
 
15
16
  /**
16
17
  * Hook to retrieve the {@link FireCMSContext}.
@@ -34,6 +35,7 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
34
35
  const dialogsController = useDialogsController();
35
36
  const customizationController = useCustomizationController();
36
37
  const analyticsController = useAnalyticsController();
38
+ const userManagement = useInternalUserManagementController<USER>();
37
39
 
38
40
  const fireCMSContextRef = React.useRef<FireCMSContext<USER, AuthControllerType>>({
39
41
  authController,
@@ -46,7 +48,8 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
46
48
  userConfigPersistence,
47
49
  dialogsController,
48
50
  customizationController,
49
- analyticsController
51
+ analyticsController,
52
+ userManagement
50
53
  });
51
54
 
52
55
  useEffect(() => {
@@ -61,7 +64,8 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
61
64
  userConfigPersistence,
62
65
  dialogsController,
63
66
  customizationController,
64
- analyticsController
67
+ analyticsController,
68
+ userManagement
65
69
  };
66
70
  }, [authController, dialogsController, navigation, sideDialogsController]);
67
71
 
@@ -0,0 +1,16 @@
1
+ import { useContext } from "react";
2
+ import { InternalUserManagement, NavigationController, User } from "../types";
3
+ import { NavigationContext } from "../contexts/NavigationContext";
4
+ import { InternalUserManagementContext } from "../contexts/InternalUserManagementContext";
5
+
6
+ /**
7
+ * Use this hook to get the internal user management of the app.
8
+ * Note that this is different from the user management plugin controller.
9
+ * This controller will be eventually replaced by the one provided
10
+ * by the user management plugin.
11
+ *
12
+ * Use at your own risk!
13
+ *
14
+ * @group Hooks and utilities
15
+ */
16
+ export const useInternalUserManagementController = <USER extends User = User>(): InternalUserManagement<USER> => useContext(InternalUserManagementContext);
@@ -31,6 +31,7 @@ import { DatePreview } from "./components/DatePreview";
31
31
  import { BooleanPreview } from "./components/BooleanPreview";
32
32
  import { NumberPropertyPreview } from "./property_previews/NumberPropertyPreview";
33
33
  import { ErrorView } from "../components";
34
+ import { UserPreview } from "./components/UserPreview";
34
35
 
35
36
  /**
36
37
  * @group Preview components
@@ -97,6 +98,13 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
97
98
  previewType={stringProperty.url}/>;
98
99
  } else if (stringProperty.markdown) {
99
100
  content = <Markdown source={value} size={"small"}/>;
101
+ } else if (stringProperty.userSelect) {
102
+ content = <UserPreview
103
+ value={value}
104
+ property={stringProperty}
105
+ propertyKey={propertyKey}
106
+ size={props.size}
107
+ />;
100
108
  } else if (stringProperty.reference) {
101
109
  if (typeof stringProperty.reference.path === "string") {
102
110
  content = <ReferencePreview
@@ -4,7 +4,7 @@ import { Entity, EntityCollection, EntityReference } from "../../types";
4
4
  import { useCustomizationController, useEntityFetch, useNavigationController } from "../../hooks";
5
5
  import { PreviewSize } from "../PropertyPreviewProps";
6
6
  import { Skeleton } from "@firecms/ui";
7
- import { ErrorView } from "../../components";
7
+ import { ErrorBoundary, ErrorView } from "../../components";
8
8
  import { EntityPreview, EntityPreviewContainer } from "../../components/EntityPreview";
9
9
 
10
10
  export type ReferencePreviewProps = {
@@ -32,7 +32,9 @@ export const ReferencePreview = function ReferencePreview(props: ReferencePrevie
32
32
  tooltip={JSON.stringify(reference)}/>
33
33
  </EntityPreviewContainer>;
34
34
  }
35
- return <ReferencePreviewInternal {...props} />;
35
+ return <ErrorBoundary>
36
+ <ReferencePreviewInternal {...props} />
37
+ </ErrorBoundary>;
36
38
  };
37
39
 
38
40
  function ReferencePreviewInternal({
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+ import { PropertyPreviewProps } from "../PropertyPreviewProps";
3
+ import { useInternalUserManagementController } from "../../hooks";
4
+ import { UserDisplay } from "../../components/UserDisplay";
5
+ import { EmptyValue } from "./EmptyValue";
6
+ import { Typography } from "@firecms/ui";
7
+
8
+ /**
9
+ * Preview component for displaying user information.
10
+ * This is a simple wrapper around UserDisplay.
11
+ *
12
+ * @group Preview components
13
+ */
14
+ export function UserPreview({ value }: PropertyPreviewProps<string>) {
15
+ const { getUser } = useInternalUserManagementController();
16
+
17
+ if (!value) {
18
+ return <EmptyValue/>;
19
+ }
20
+
21
+ const user = getUser(value);
22
+ if (!user) {
23
+ return <Typography variant={"caption"} color={"secondary"}>User not found: {value}</Typography>;
24
+ }
25
+
26
+ return <UserDisplay user={user}/>;
27
+ }
@@ -25,3 +25,4 @@ export * from "./components/EnumValuesChip";
25
25
  export * from "./components/EmptyValue";
26
26
  export * from "./components/ImagePreview";
27
27
  export * from "./components/ReferencePreview";
28
+ export * from "./components/UserPreview";
@@ -42,7 +42,7 @@ export function ArrayPropertyPreview({
42
42
  const childSize: PreviewSize = size === "medium" ? "medium" : "small";
43
43
 
44
44
  return (
45
- <div className="flex flex-col gap-2">
45
+ <div className="w-full flex flex-col gap-2">
46
46
  {values &&
47
47
  values.map((value, index) => {
48
48
  const of: ResolvedProperty = property.resolvedProperties[index] ??
@@ -69,6 +69,8 @@ export interface EntityCollection<M extends Record<string, any> = any, USER exte
69
69
  * https://fonts.google.com/icons
70
70
  * e.g. 'account_tree' or 'person'.
71
71
  * Find all the icons in https://firecms.co/docs/icons
72
+ * You can also pass a React node if you want to render a custom icon.
73
+ * If not specified, a default icon will be used.
72
74
  */
73
75
  icon?: string | React.ReactNode;
74
76
 
@@ -50,9 +50,15 @@ export class EntityReference {
50
50
  */
51
51
  readonly path: string;
52
52
 
53
- constructor(id: string, path: string) {
53
+ /**
54
+ * Optional database ID where the entity is stored (if multiple databases are used)
55
+ */
56
+ readonly databaseId?: string;
57
+
58
+ constructor(id: string, path: string, databaseId?: string) {
54
59
  this.id = id;
55
60
  this.path = path;
61
+ this.databaseId = databaseId;
56
62
  }
57
63
 
58
64
  get pathWithId() {
@@ -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
  /**
18
19
  * Use this callback to build entity collections dynamically.
@@ -139,6 +140,7 @@ export type FireCMSProps<USER extends User> = {
139
140
  * Use plugins to modify the behaviour of the CMS.
140
141
  * DEPRECATED: use the `plugins` prop in the `useBuildNavigationController` instead.
141
142
  * This prop will work as a fallback for the `plugins` prop in the `useBuildNavigationController`.
143
+ * @deprecated
142
144
  */
143
145
  plugins?: FireCMSPlugin<any, any, any>[];
144
146
 
@@ -153,6 +155,20 @@ export type FireCMSProps<USER extends User> = {
153
155
  */
154
156
  entityLinkBuilder?: EntityLinkBuilder;
155
157
 
158
+ /**
159
+ * You can use this props to provide your own user management implementation.
160
+ * Note that this will not affect the UI, but it will be used to show user information
161
+ * in various places of the CMS, for example, to show who created or modified an entity,
162
+ * or to assign ownership of an entity.
163
+ *
164
+ * You can also use this data to be retrieved in your custom properties,
165
+ * for example, to show a list of users in a dropdown.
166
+ *
167
+ * If you are using the FireCMS user management plugin, this
168
+ * prop will be implemented automatically.
169
+ */
170
+ userManagement?: InternalUserManagement
171
+
156
172
  components?: {
157
173
 
158
174
  /**
@@ -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
  /**
15
16
  * Context that includes the internal controllers and contexts used by the app.
@@ -80,4 +81,20 @@ export type FireCMSContext<USER extends User = User, AuthControllerType extends
80
81
  */
81
82
  analyticsController?: AnalyticsController;
82
83
 
84
+ /**
85
+ * This section is used to manage users in the CMS.
86
+ * It is used to show user information in various places of the CMS,
87
+ * for example, to show who created or modified an entity,
88
+ * or to assign ownership of an entity.
89
+ *
90
+ * In the base CMS, this information is not used for access control.
91
+ * You can pass your own implementation of this section, to populate
92
+ * the dropdown of users when assigning ownership of an entity,
93
+ * or to show more information about the user.
94
+ *
95
+ * If you are using the FireCMS user management plugin, this
96
+ * section will be implemented automatically.
97
+ */
98
+ userManagement: InternalUserManagement<USER>
99
+
83
100
  };
@@ -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,24 @@
1
+ import { User } from "./user";
2
+
3
+ export type InternalUserManagement<USER extends User = User> = {
4
+
5
+ /**
6
+ * List of users to be managed by the CMS.
7
+ */
8
+ users: USER[];
9
+
10
+ /**
11
+ * Function to get a user by its uid. This is used to show
12
+ * user information when assigning ownership of an entity.
13
+ *
14
+ * You can pass your own implementation if you want to show
15
+ * more information about the user.
16
+ *
17
+ * If you are using the FireCMS user management plugin, this
18
+ * function will be implemented automatically.
19
+ *
20
+ * @param uid
21
+ */
22
+ getUser: (uid: string) => USER | null;
23
+
24
+ }
@@ -8,6 +8,7 @@ import { CMSType, Property } from "./properties";
8
8
  import { EntityStatus } from "./entities";
9
9
  import { ResolvedProperty } from "./resolved_entities";
10
10
  import { NavigationGroupMapping } from "./navigation";
11
+ import { InternalUserManagement } from "./internal_user_management";
11
12
 
12
13
  /**
13
14
  * Interface used to define plugins for FireCMS.
@@ -43,6 +44,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
43
44
  props?: PROPS;
44
45
  };
45
46
 
47
+ userManagement?: InternalUserManagement
48
+
46
49
  homePage?: {
47
50
 
48
51
  /**
@@ -388,6 +388,16 @@ export interface StringProperty extends BaseProperty<string> {
388
388
  */
389
389
  storage?: StorageConfig;
390
390
 
391
+ /**
392
+ * This property is used to indicate that the string is a user ID, and
393
+ * it will be rendered as a user picker.
394
+ * Note that the user ID needs to be the one used in your authentication
395
+ * provider, e.g. Firebase Auth.
396
+ * You can also use a property builder to specify the user path dynamically
397
+ * based on other values of the entity.
398
+ */
399
+ userSelect?: boolean;
400
+
391
401
  /**
392
402
  * If the value of this property is a URL, you can set this flag to true
393
403
  * to add a link, or one of the supported media types to render a preview
@@ -52,6 +52,7 @@ export type PropertyConfigId =
52
52
  "markdown" |
53
53
  "url" |
54
54
  "email" |
55
+ "user_select" |
55
56
  "select" |
56
57
  "multi_select" |
57
58
  "number_input" |
package/src/types/user.ts CHANGED
@@ -37,7 +37,7 @@ export type User = {
37
37
  readonly isAnonymous: boolean;
38
38
 
39
39
  /**
40
- *
40
+ * Custom roles assigned to the user.
41
41
  */
42
42
  roles?: Role[];
43
43
 
@@ -142,7 +142,7 @@ export function sanitizeData<M extends Record<string, any>>
142
142
  }
143
143
 
144
144
  export function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M>): EntityReference {
145
- return new EntityReference(entity.id, entity.path);
145
+ return new EntityReference(entity.id, entity.path, entity.databaseId);
146
146
  }
147
147
 
148
148
  export function traverseValuesProperties<M extends Record<string, any>>(