@rebasepro/app 0.0.1-canary.4829d6e
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/auth/api.d.ts +66 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/types.d.ts +127 -0
- package/dist/auth/useRebaseAuthController.d.ts +13 -0
- package/dist/components/AIIcon.d.ts +17 -0
- package/dist/components/AuthSimulationMenu.d.ts +0 -0
- package/dist/components/ConfirmationDialog.d.ts +9 -0
- package/dist/components/Debug/UIReferenceView.d.ts +13 -0
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
- package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/index.d.ts +3 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
- package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
- package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
- package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
- package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
- package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
- package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
- package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
- package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
- package/dist/components/ErrorTooltip.d.ts +3 -0
- package/dist/components/ErrorView.d.ts +21 -0
- package/dist/components/LanguageToggle.d.ts +2 -0
- package/dist/components/LoginView/LoginView.d.ts +113 -0
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/NotFoundPage.d.ts +2 -0
- package/dist/components/RebaseAuth.d.ts +10 -0
- package/dist/components/RebaseLogo.d.ts +7 -0
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +9 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/UserSelectPopover.d.ts +63 -0
- package/dist/components/UserSettingsView.d.ts +2 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +66 -0
- package/dist/components/common/useColumnsIds.d.ts +9 -0
- package/dist/components/common/useDataTableController.d.ts +45 -0
- package/dist/components/common/useDebouncedData.d.ts +9 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/contexts/AdminModeController.d.ts +4 -0
- package/dist/contexts/AnalyticsContext.d.ts +3 -0
- package/dist/contexts/AuthControllerContext.d.ts +3 -0
- package/dist/contexts/CollectionScopeContext.d.ts +50 -0
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
- package/dist/contexts/DataDriverContext.d.ts +3 -0
- package/dist/contexts/DataSourcesContext.d.ts +31 -0
- package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
- package/dist/contexts/DialogsProvider.d.ts +4 -0
- package/dist/contexts/EffectiveRoleController.d.ts +4 -0
- package/dist/contexts/ModeController.d.ts +4 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
- package/dist/contexts/RebaseDataContext.d.ts +3 -0
- package/dist/contexts/SnackbarProvider.d.ts +2 -0
- package/dist/contexts/StorageSourceContext.d.ts +3 -0
- package/dist/contexts/StorageSourcesContext.d.ts +25 -0
- package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
- package/dist/contexts/index.d.ts +17 -0
- package/dist/core/PluginLifecycleManager.d.ts +17 -0
- package/dist/core/PluginProviderStack.d.ts +21 -0
- package/dist/core/Rebase.d.ts +15 -0
- package/dist/core/RebaseProps.d.ts +291 -0
- package/dist/core/RebaseRouter.d.ts +4 -0
- package/dist/core/RebaseRoutes.d.ts +17 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/hooks/ApiConfigContext.d.ts +24 -0
- package/dist/hooks/data/delete.d.ts +33 -0
- package/dist/hooks/data/save.d.ts +40 -0
- package/dist/hooks/data/useCollection.d.ts +62 -0
- package/dist/hooks/data/useData.d.ts +13 -0
- package/dist/hooks/data/useDataOrder.d.ts +12 -0
- package/dist/hooks/data/useFetch.d.ts +43 -0
- package/dist/hooks/data/useRelationSelector.d.ts +52 -0
- package/dist/hooks/index.d.ts +36 -0
- package/dist/hooks/useAdminModeController.d.ts +19 -0
- package/dist/hooks/useAnalyticsController.d.ts +5 -0
- package/dist/hooks/useAuthController.d.ts +11 -0
- package/dist/hooks/useAuthSubscription.d.ts +2 -0
- package/dist/hooks/useBackendStorageSource.d.ts +30 -0
- package/dist/hooks/useBridgeRegistration.d.ts +18 -0
- package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
- package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
- package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
- package/dist/hooks/useBuildModeController.d.ts +6 -0
- package/dist/hooks/useClipboard.d.ts +57 -0
- package/dist/hooks/useCollapsedGroups.d.ts +27 -0
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useCustomizationController.d.ts +11 -0
- package/dist/hooks/useDialogsController.d.ts +11 -0
- package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
- package/dist/hooks/useLargeLayout.d.ts +1 -0
- package/dist/hooks/useModeController.d.ts +19 -0
- package/dist/hooks/usePermissions.d.ts +12 -0
- package/dist/hooks/useRebaseClient.d.ts +5 -0
- package/dist/hooks/useRebaseContext.d.ts +11 -0
- package/dist/hooks/useRebaseRegistry.d.ts +34 -0
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/hooks/useSlot.d.ts +18 -0
- package/dist/hooks/useSnackbarController.d.ts +20 -0
- package/dist/hooks/useStorageSource.d.ts +6 -0
- package/dist/hooks/useStudioBridge.d.ts +79 -0
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
- package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
- package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.es.js +16843 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +17021 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/internal/common.d.ts +14 -0
- package/dist/internal/useRestoreScroll.d.ts +15 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/util/constants.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +22 -0
- package/dist/util/enums.d.ts +5 -0
- package/dist/util/icon_list.d.ts +5 -0
- package/dist/util/icons.d.ts +20 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/previews.d.ts +4 -0
- package/dist/util/useStorageUploadController.d.ts +44 -0
- package/dist/vitePlugin.d.ts +60 -0
- package/dist/vitePlugin.js +113 -0
- package/package.json +145 -0
- package/src/auth/api.ts +148 -0
- package/src/auth/index.ts +25 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/useRebaseAuthController.ts +328 -0
- package/src/components/AIIcon.tsx +47 -0
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/Debug/UIReferenceView.tsx +958 -0
- package/src/components/Debug/UIStyleGuide.tsx +156 -0
- package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
- package/src/components/Debug/collection-views/index.ts +3 -0
- package/src/components/Debug/collection-views/sample_data.ts +42 -0
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
- package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
- package/src/components/ErrorTooltip.tsx +12 -0
- package/src/components/ErrorView.tsx +94 -0
- package/src/components/LanguageToggle.tsx +59 -0
- package/src/components/LoginView/LoginView.tsx +932 -0
- package/src/components/LoginView/index.ts +2 -0
- package/src/components/NotFoundPage.tsx +27 -0
- package/src/components/RebaseAuth.tsx +27 -0
- package/src/components/RebaseLogo.tsx +29 -0
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +54 -0
- package/src/components/UserSelectPopover.tsx +399 -0
- package/src/components/UserSettingsView.tsx +318 -0
- package/src/components/common/index.ts +6 -0
- package/src/components/common/table_height.tsx +21 -0
- package/src/components/common/types.tsx +66 -0
- package/src/components/common/useColumnsIds.tsx +222 -0
- package/src/components/common/useDataTableController.tsx +514 -0
- package/src/components/common/useDebouncedData.ts +49 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/index.tsx +26 -0
- package/src/contexts/AdminModeController.tsx +11 -0
- package/src/contexts/AnalyticsContext.tsx +4 -0
- package/src/contexts/AuthControllerContext.tsx +4 -0
- package/src/contexts/CollectionScopeContext.tsx +67 -0
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/CustomizationControllerContext.tsx +4 -0
- package/src/contexts/DataDriverContext.tsx +4 -0
- package/src/contexts/DataSourcesContext.tsx +36 -0
- package/src/contexts/DatabaseAdminContext.tsx +4 -0
- package/src/contexts/DialogsProvider.tsx +56 -0
- package/src/contexts/EffectiveRoleController.tsx +12 -0
- package/src/contexts/ModeController.tsx +11 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
- package/src/contexts/RebaseDataContext.tsx +4 -0
- package/src/contexts/SnackbarProvider.tsx +15 -0
- package/src/contexts/StorageSourceContext.tsx +4 -0
- package/src/contexts/StorageSourcesContext.tsx +30 -0
- package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
- package/src/contexts/index.ts +17 -0
- package/src/core/PluginLifecycleManager.tsx +95 -0
- package/src/core/PluginProviderStack.tsx +40 -0
- package/src/core/Rebase.tsx +439 -0
- package/src/core/RebaseProps.tsx +320 -0
- package/src/core/RebaseRouter.tsx +17 -0
- package/src/core/RebaseRoutes.tsx +24 -0
- package/src/core/index.tsx +5 -0
- package/src/hooks/ApiConfigContext.tsx +41 -0
- package/src/hooks/data/delete.ts +55 -0
- package/src/hooks/data/save.ts +77 -0
- package/src/hooks/data/useCollection.tsx +176 -0
- package/src/hooks/data/useData.tsx +18 -0
- package/src/hooks/data/useDataOrder.ts +26 -0
- package/src/hooks/data/useFetch.tsx +140 -0
- package/src/hooks/data/useRelationSelector.tsx +234 -0
- package/src/hooks/index.tsx +45 -0
- package/src/hooks/useAdminModeController.tsx +23 -0
- package/src/hooks/useAnalyticsController.tsx +8 -0
- package/src/hooks/useAuthController.tsx +14 -0
- package/src/hooks/useAuthSubscription.ts +87 -0
- package/src/hooks/useBackendStorageSource.ts +288 -0
- package/src/hooks/useBridgeRegistration.tsx +32 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
- package/src/hooks/useBuildAdminModeController.tsx +24 -0
- package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
- package/src/hooks/useBuildModeController.tsx +71 -0
- package/src/hooks/useClipboard.tsx +158 -0
- package/src/hooks/useCollapsedGroups.ts +116 -0
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useCustomizationController.tsx +14 -0
- package/src/hooks/useDialogsController.tsx +15 -0
- package/src/hooks/useEffectiveRoleController.tsx +10 -0
- package/src/hooks/useLargeLayout.tsx +65 -0
- package/src/hooks/useModeController.tsx +23 -0
- package/src/hooks/usePermissions.ts +44 -0
- package/src/hooks/useRebaseClient.tsx +10 -0
- package/src/hooks/useRebaseContext.tsx +83 -0
- package/src/hooks/useRebaseRegistry.tsx +91 -0
- package/src/hooks/useResolvedComponent.tsx +159 -0
- package/src/hooks/useSlot.tsx +64 -0
- package/src/hooks/useSnackbarController.tsx +52 -0
- package/src/hooks/useStorageSource.tsx +11 -0
- package/src/hooks/useStudioBridge.tsx +209 -0
- package/src/hooks/useTranslation.ts +33 -0
- package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
- package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
- package/src/i18n/RebaseI18nProvider.tsx +160 -0
- package/src/index.ts +25 -0
- package/src/internal/common.tsx +16 -0
- package/src/internal/useRestoreScroll.tsx +69 -0
- package/src/locales/de.ts +902 -0
- package/src/locales/en.ts +980 -0
- package/src/locales/es.ts +922 -0
- package/src/locales/fr.ts +901 -0
- package/src/locales/hi.ts +901 -0
- package/src/locales/it.ts +901 -0
- package/src/locales/pt.ts +909 -0
- package/src/util/constants.ts +7 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entity_cache.ts +239 -0
- package/src/util/enums.ts +32 -0
- package/src/util/icon_list.ts +22 -0
- package/src/util/icons.tsx +149 -0
- package/src/util/index.ts +12 -0
- package/src/util/previews.ts +105 -0
- package/src/util/useStorageUploadController.tsx +350 -0
- package/src/vitePlugin.ts +196 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
|
|
2
|
+
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
CircleUserIcon,
|
|
5
|
+
CircularProgress,
|
|
6
|
+
cls,
|
|
7
|
+
defaultBorderMixin,
|
|
8
|
+
IconButton,
|
|
9
|
+
iconSize,
|
|
10
|
+
Popover,
|
|
11
|
+
SearchBar,
|
|
12
|
+
SearchIcon,
|
|
13
|
+
Typography,
|
|
14
|
+
XIcon
|
|
15
|
+
} from "@rebasepro/ui";
|
|
16
|
+
import { User } from "@rebasepro/types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a selectable user item with optional role information.
|
|
20
|
+
*/
|
|
21
|
+
export interface SelectableUser {
|
|
22
|
+
uid: string;
|
|
23
|
+
displayName?: string | null;
|
|
24
|
+
email?: string | null;
|
|
25
|
+
photoURL?: string | null;
|
|
26
|
+
roles?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UserSelectPopoverProps {
|
|
30
|
+
/**
|
|
31
|
+
* The currently selected user, or `null` for "self"/default.
|
|
32
|
+
*/
|
|
33
|
+
selectedUser: SelectableUser | null;
|
|
34
|
+
/**
|
|
35
|
+
* Called when the user selection changes.
|
|
36
|
+
* `null` means "reset to self/default".
|
|
37
|
+
*/
|
|
38
|
+
onUserSelected: (user: SelectableUser | null) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Full list of users to display.
|
|
41
|
+
* The component handles client-side search/filter internally.
|
|
42
|
+
*/
|
|
43
|
+
users: SelectableUser[];
|
|
44
|
+
/**
|
|
45
|
+
* Whether users are currently loading.
|
|
46
|
+
*/
|
|
47
|
+
loading?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The current user (displayed as the "self" option at the top).
|
|
50
|
+
*/
|
|
51
|
+
currentUser?: SelectableUser | null;
|
|
52
|
+
/**
|
|
53
|
+
* Label to display when no user is selected.
|
|
54
|
+
* @default "Current user"
|
|
55
|
+
*/
|
|
56
|
+
defaultLabel?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Maximum number of users to render at a time (for performance).
|
|
59
|
+
* Users can still be found via search.
|
|
60
|
+
* @default 100
|
|
61
|
+
*/
|
|
62
|
+
renderLimit?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Additional class name for the trigger button.
|
|
65
|
+
*/
|
|
66
|
+
className?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Size variant.
|
|
69
|
+
* @default "small"
|
|
70
|
+
*/
|
|
71
|
+
size?: "small" | "medium";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const ITEM_HEIGHT = 44;
|
|
75
|
+
const MAX_VISIBLE_ITEMS = 7;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A reusable user picker popover with search support.
|
|
79
|
+
* Designed to handle very large user lists (100k+) by filtering
|
|
80
|
+
* client-side and limiting the rendered set.
|
|
81
|
+
*
|
|
82
|
+
* @group Components
|
|
83
|
+
*/
|
|
84
|
+
export function UserSelectPopover({
|
|
85
|
+
selectedUser,
|
|
86
|
+
onUserSelected,
|
|
87
|
+
users,
|
|
88
|
+
loading = false,
|
|
89
|
+
currentUser,
|
|
90
|
+
defaultLabel = "Current user",
|
|
91
|
+
renderLimit = 100,
|
|
92
|
+
className,
|
|
93
|
+
size = "small"
|
|
94
|
+
}: UserSelectPopoverProps) {
|
|
95
|
+
const [open, setOpen] = useState(false);
|
|
96
|
+
const [searchText, setSearchText] = useState("");
|
|
97
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
98
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
99
|
+
|
|
100
|
+
// Effective user label for the trigger
|
|
101
|
+
const effectiveUserLabel = selectedUser
|
|
102
|
+
? (selectedUser.displayName || selectedUser.email || selectedUser.uid)
|
|
103
|
+
: (currentUser?.displayName || currentUser?.email || defaultLabel);
|
|
104
|
+
|
|
105
|
+
// Filter users based on search text
|
|
106
|
+
const filteredUsers = useMemo(() => {
|
|
107
|
+
const query = searchText.trim().toLowerCase();
|
|
108
|
+
const base = users.filter(u => u.uid !== currentUser?.uid);
|
|
109
|
+
|
|
110
|
+
if (!query) return base.slice(0, renderLimit);
|
|
111
|
+
|
|
112
|
+
const matched = base.filter(u => {
|
|
113
|
+
const name = (u.displayName || "").toLowerCase();
|
|
114
|
+
const email = (u.email || "").toLowerCase();
|
|
115
|
+
const uid = u.uid.toLowerCase();
|
|
116
|
+
const roles = (u.roles || []).join(" ").toLowerCase();
|
|
117
|
+
return (
|
|
118
|
+
name.includes(query) ||
|
|
119
|
+
email.includes(query) ||
|
|
120
|
+
uid.includes(query) ||
|
|
121
|
+
roles.includes(query)
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return matched.slice(0, renderLimit);
|
|
126
|
+
}, [users, currentUser?.uid, searchText, renderLimit]);
|
|
127
|
+
|
|
128
|
+
const totalFilterable = useMemo(() => {
|
|
129
|
+
const query = searchText.trim().toLowerCase();
|
|
130
|
+
const base = users.filter(u => u.uid !== currentUser?.uid);
|
|
131
|
+
if (!query) return base.length;
|
|
132
|
+
return base.filter(u => {
|
|
133
|
+
const name = (u.displayName || "").toLowerCase();
|
|
134
|
+
const email = (u.email || "").toLowerCase();
|
|
135
|
+
const uid = u.uid.toLowerCase();
|
|
136
|
+
const roles = (u.roles || []).join(" ").toLowerCase();
|
|
137
|
+
return name.includes(query) || email.includes(query) || uid.includes(query) || roles.includes(query);
|
|
138
|
+
}).length;
|
|
139
|
+
}, [users, currentUser?.uid, searchText]);
|
|
140
|
+
|
|
141
|
+
const handleSelect = useCallback((user: SelectableUser | null) => {
|
|
142
|
+
onUserSelected(user);
|
|
143
|
+
setOpen(false);
|
|
144
|
+
setSearchText("");
|
|
145
|
+
}, [onUserSelected]);
|
|
146
|
+
|
|
147
|
+
const handleOpenChange = useCallback((isOpen: boolean) => {
|
|
148
|
+
setOpen(isOpen);
|
|
149
|
+
if (isOpen) {
|
|
150
|
+
// Auto-focus the search input when opening
|
|
151
|
+
setTimeout(() => inputRef.current?.focus(), 50);
|
|
152
|
+
} else {
|
|
153
|
+
setSearchText("");
|
|
154
|
+
}
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
158
|
+
if (e.key === "Escape") {
|
|
159
|
+
setOpen(false);
|
|
160
|
+
setSearchText("");
|
|
161
|
+
}
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const listHeight = Math.min(
|
|
165
|
+
(filteredUsers.length + 1) * ITEM_HEIGHT, // +1 for the "self" option
|
|
166
|
+
MAX_VISIBLE_ITEMS * ITEM_HEIGHT
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<Popover
|
|
171
|
+
open={open}
|
|
172
|
+
onOpenChange={handleOpenChange}
|
|
173
|
+
side="bottom"
|
|
174
|
+
align="end"
|
|
175
|
+
sideOffset={4}
|
|
176
|
+
trigger={
|
|
177
|
+
<button
|
|
178
|
+
className={cls(
|
|
179
|
+
"flex items-center gap-1.5 text-xs font-medium transition-colors rounded border border-transparent",
|
|
180
|
+
"text-text-secondary dark:text-text-secondary-dark",
|
|
181
|
+
"hover:text-text-primary dark:hover:text-text-primary-dark",
|
|
182
|
+
"bg-surface-100 hover:bg-surface-200 dark:bg-surface-950 dark:hover:bg-surface-700",
|
|
183
|
+
size === "small" ? "px-2 py-1" : "px-3 py-1.5",
|
|
184
|
+
className
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
{selectedUser?.photoURL ? (
|
|
188
|
+
<img
|
|
189
|
+
src={selectedUser.photoURL}
|
|
190
|
+
alt=""
|
|
191
|
+
className="w-4 h-4 rounded-full object-cover"
|
|
192
|
+
/>
|
|
193
|
+
) : (
|
|
194
|
+
<svg
|
|
195
|
+
className="w-3.5 h-3.5 text-text-disabled dark:text-text-disabled-dark"
|
|
196
|
+
fill="none"
|
|
197
|
+
stroke="currentColor"
|
|
198
|
+
viewBox="0 0 24 24"
|
|
199
|
+
>
|
|
200
|
+
<path
|
|
201
|
+
strokeLinecap="round"
|
|
202
|
+
strokeLinejoin="round"
|
|
203
|
+
strokeWidth={2}
|
|
204
|
+
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
|
205
|
+
/>
|
|
206
|
+
</svg>
|
|
207
|
+
)}
|
|
208
|
+
<span className="max-w-[120px] truncate">{effectiveUserLabel}</span>
|
|
209
|
+
{selectedUser && (
|
|
210
|
+
<span className="text-[9px] text-amber-600 dark:text-amber-400 font-bold uppercase">
|
|
211
|
+
impersonating
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
<svg className="w-3 h-3 opacity-50 ml-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
215
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7"/>
|
|
216
|
+
</svg>
|
|
217
|
+
</button>
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
<div
|
|
221
|
+
className="flex flex-col w-[320px] max-w-[90vw] overflow-hidden"
|
|
222
|
+
onKeyDown={handleKeyDown}
|
|
223
|
+
>
|
|
224
|
+
{/* Header */}
|
|
225
|
+
<div className={cls("px-3 py-2 border-b flex items-center justify-between shrink-0", defaultBorderMixin)}>
|
|
226
|
+
<Typography
|
|
227
|
+
variant="caption"
|
|
228
|
+
className="font-bold uppercase tracking-wider text-[9px] text-text-disabled dark:text-text-disabled-dark"
|
|
229
|
+
>
|
|
230
|
+
Run as user
|
|
231
|
+
</Typography>
|
|
232
|
+
{users.length > 0 && (
|
|
233
|
+
<Typography
|
|
234
|
+
variant="caption"
|
|
235
|
+
className="text-[9px] text-text-disabled dark:text-text-disabled-dark tabular-nums"
|
|
236
|
+
>
|
|
237
|
+
{totalFilterable} user{totalFilterable !== 1 ? "s" : ""}
|
|
238
|
+
</Typography>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* Search input */}
|
|
243
|
+
<div className={cls("px-2 py-1.5 border-b shrink-0", defaultBorderMixin)}>
|
|
244
|
+
<SearchBar
|
|
245
|
+
inputRef={inputRef}
|
|
246
|
+
size="smallest"
|
|
247
|
+
placeholder="Search by name, email, or role…"
|
|
248
|
+
onTextSearch={(val) => setSearchText(val ?? "")}
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{/* User list */}
|
|
253
|
+
<div
|
|
254
|
+
ref={listRef}
|
|
255
|
+
className="overflow-y-auto overscroll-contain"
|
|
256
|
+
style={{ maxHeight: MAX_VISIBLE_ITEMS * ITEM_HEIGHT }}
|
|
257
|
+
>
|
|
258
|
+
{/* Self (default) option — always shown unless search hides it */}
|
|
259
|
+
{currentUser && (!searchText.trim() || matchesSearch(currentUser, searchText)) && (
|
|
260
|
+
<UserRow
|
|
261
|
+
user={currentUser}
|
|
262
|
+
isSelected={!selectedUser}
|
|
263
|
+
isSelf
|
|
264
|
+
onClick={() => handleSelect(null)}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{/* Loading state */}
|
|
269
|
+
{loading && (
|
|
270
|
+
<div className="flex items-center justify-center py-6">
|
|
271
|
+
<CircularProgress size="small"/>
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Empty state */}
|
|
276
|
+
{!loading && filteredUsers.length === 0 && searchText.trim() && (
|
|
277
|
+
<div className="flex flex-col items-center justify-center py-6 px-4">
|
|
278
|
+
<Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark text-center">
|
|
279
|
+
No users matching “{searchText}”
|
|
280
|
+
</Typography>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{/* User items */}
|
|
285
|
+
{!loading && filteredUsers.map(user => (
|
|
286
|
+
<UserRow
|
|
287
|
+
key={user.uid}
|
|
288
|
+
user={user}
|
|
289
|
+
isSelected={selectedUser?.uid === user.uid}
|
|
290
|
+
onClick={() => handleSelect(user)}
|
|
291
|
+
/>
|
|
292
|
+
))}
|
|
293
|
+
|
|
294
|
+
{/* Truncation notice */}
|
|
295
|
+
{!loading && totalFilterable > renderLimit && (
|
|
296
|
+
<div className={cls("px-3 py-2 border-t text-center", defaultBorderMixin)}>
|
|
297
|
+
<Typography
|
|
298
|
+
variant="caption"
|
|
299
|
+
className="text-[10px] text-text-disabled dark:text-text-disabled-dark"
|
|
300
|
+
>
|
|
301
|
+
Showing {renderLimit} of {totalFilterable} results. Use search to narrow down.
|
|
302
|
+
</Typography>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
</Popover>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── Internal Components ─────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
interface UserRowProps {
|
|
314
|
+
user: SelectableUser;
|
|
315
|
+
isSelected: boolean;
|
|
316
|
+
isSelf?: boolean;
|
|
317
|
+
onClick: () => void;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function UserRow({ user, isSelected, isSelf, onClick }: UserRowProps) {
|
|
321
|
+
return (
|
|
322
|
+
<button
|
|
323
|
+
onClick={onClick}
|
|
324
|
+
className={cls(
|
|
325
|
+
"w-full flex items-center gap-2.5 px-3 py-2 text-left transition-colors",
|
|
326
|
+
"hover:bg-surface-accent-100 dark:hover:bg-surface-accent-900",
|
|
327
|
+
isSelected && "bg-primary/5 dark:bg-primary-dark/10"
|
|
328
|
+
)}
|
|
329
|
+
style={{ minHeight: ITEM_HEIGHT }}
|
|
330
|
+
>
|
|
331
|
+
{/* Avatar */}
|
|
332
|
+
{user.photoURL ? (
|
|
333
|
+
<img
|
|
334
|
+
src={user.photoURL}
|
|
335
|
+
alt=""
|
|
336
|
+
className="w-7 h-7 rounded-full object-cover shrink-0"
|
|
337
|
+
/>
|
|
338
|
+
) : (
|
|
339
|
+
<div className="w-7 h-7 rounded-full bg-surface-200 dark:bg-surface-700 flex items-center justify-center shrink-0">
|
|
340
|
+
<CircleUserIcon
|
|
341
|
+
size={"smallest"}
|
|
342
|
+
className="text-text-disabled dark:text-text-disabled-dark"
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{/* User info */}
|
|
348
|
+
<div className="flex flex-col min-w-0 flex-grow">
|
|
349
|
+
<div className="flex items-center gap-1.5">
|
|
350
|
+
<span
|
|
351
|
+
className={cls(
|
|
352
|
+
"text-xs font-medium truncate",
|
|
353
|
+
isSelected
|
|
354
|
+
? "text-primary dark:text-primary-dark"
|
|
355
|
+
: "text-text-primary dark:text-text-primary-dark"
|
|
356
|
+
)}
|
|
357
|
+
>
|
|
358
|
+
{user.displayName || user.email || user.uid}
|
|
359
|
+
</span>
|
|
360
|
+
{isSelf && (
|
|
361
|
+
<span className="text-[9px] text-text-disabled dark:text-text-disabled-dark font-medium shrink-0">
|
|
362
|
+
(you)
|
|
363
|
+
</span>
|
|
364
|
+
)}
|
|
365
|
+
</div>
|
|
366
|
+
{(user.email || user.roles?.length) && (
|
|
367
|
+
<span className="text-[10px] text-text-disabled dark:text-text-disabled-dark truncate">
|
|
368
|
+
{user.email && user.displayName ? user.email : ""}
|
|
369
|
+
{user.email && user.displayName && user.roles?.length ? " · " : ""}
|
|
370
|
+
{user.roles?.length ? user.roles.join(", ") : ""}
|
|
371
|
+
</span>
|
|
372
|
+
)}
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
{/* Selection indicator */}
|
|
376
|
+
{isSelected && (
|
|
377
|
+
<svg
|
|
378
|
+
className="w-4 h-4 text-primary dark:text-primary-dark shrink-0"
|
|
379
|
+
fill="none"
|
|
380
|
+
stroke="currentColor"
|
|
381
|
+
viewBox="0 0 24 24"
|
|
382
|
+
>
|
|
383
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7"/>
|
|
384
|
+
</svg>
|
|
385
|
+
)}
|
|
386
|
+
</button>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
function matchesSearch(user: SelectableUser, searchText: string): boolean {
|
|
393
|
+
const query = searchText.trim().toLowerCase();
|
|
394
|
+
if (!query) return true;
|
|
395
|
+
const name = (user.displayName || "").toLowerCase();
|
|
396
|
+
const email = (user.email || "").toLowerCase();
|
|
397
|
+
const uid = user.uid.toLowerCase();
|
|
398
|
+
return name.includes(query) || email.includes(query) || uid.includes(query);
|
|
399
|
+
}
|