) => void
+ activationLink: string | null
+ onGeneratePassword: () => void
+}) {
+ const { t } = useTranslation()
+
+ return (
+ <>
+ You can also manually set a new password for the user.
+
+ {t('new_password')}
+
+
+
+ }
+ />
+
+
+
+ You can also manually send them URLs below to allow them to reset their
+ password and log in for the first time.
+
+ The password reset link or randomly generated password is below:
+
+
+ {activationLink}
+
+
+
+
+ >
+ )
+}
+
+function UserFeaturesTab({
+ userData,
+ handleFeatureNumChange,
+ autoFocusedRef,
+}: {
+ userData: any
+ handleFeatureNumChange: (e: React.ChangeEvent) => void
+ autoFocusedRef: React.Ref
+}) {
+ return (
+ <>
+
+ Compile Timeout (In second, no more than 300s)
+
+
+
+ Collaborator limit (use -1 for unlimited)
+
+
+ >
+ )
+}
+
+function UpdateUserModal({
+ users,
+ actionHandler,
+ showModal,
+ handleCloseModal,
+}: UpdateUserModalProps) {
+ const { t } = useTranslation()
+ const { autoFocusedRef } = useRefWithAutoFocus()
+ const [activeTab, setActiveTab] = useState('basic-info')
+
+ if (users.length !== 1) return null
+ const [userData, setUserData] = useState(pickUserFields(users[0]))
+ const isSelf = getMeta('ol-user_id') === users[0].id
+ const allowUpdateDetails = users[0].allowUpdateDetails
+ const allowUpdateIsAdmin = users[0].allowUpdateIsAdmin
+ const [activationLink, setActivationLink] = useState(null)
+
+ useEffect(() => {
+ if (showModal) {
+ setUserData(pickUserFields(users[0]))
+ setActiveTab('basic-info')
+
+ getAdditionalUserInfo(users[0].id)
+ .then(({ activationLink }) => {
+ setActivationLink(activationLink)
+ })
+ .catch(() => {
+ setActivationLink(null)
+ })
+ }
+ }, [showModal, users])
+
+ const handleTextChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.currentTarget
+ setUserData(prev => ({ ...prev, [name]: value }))
+ }
+
+ const handleCheckboxChange = (e: React.ChangeEvent) => {
+ const { name, checked } = e.currentTarget
+ setUserData(prev => ({ ...prev, [name]: checked }))
+ }
+
+ const handleFeatureNumChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.currentTarget
+ const numberValue = parseInt(value, 10)
+ if (!isNaN(numberValue)) {
+ setUserData(prev => ({
+ ...prev,
+ features: {
+ ...prev.features,
+ [name]: numberValue,
+ },
+ }))
+ }
+ }
+
+ const generatePassword = () => {
+ const PASSWORD_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ0123456789'
+ const length = 12
+ const values = new Uint32Array(length)
+ window.crypto.getRandomValues(values)
+
+ let password = ''
+ for (let i = 0; i < length; i++) {
+ password += PASSWORD_CHARS[values[i] % PASSWORD_CHARS.length]
+ }
+ setUserData(prev => ({ ...prev, password }))
+ setActivationLink(password)
+ }
+
+ return (
+
+
)
}
diff --git a/services/web/modules/admin-tools/frontend/js/user-list/components/table/user-list-table.tsx b/services/web/modules/admin-tools/frontend/js/user-list/components/table/user-list-table.tsx
index 3df582f676..c307eec86a 100644
--- a/services/web/modules/admin-tools/frontend/js/user-list/components/table/user-list-table.tsx
+++ b/services/web/modules/admin-tools/frontend/js/user-list/components/table/user-list-table.tsx
@@ -190,7 +190,7 @@ function UserListTable() {
) : (
|
- {t('no_users')}
+ {t('no_search_results')}
|
)}
diff --git a/services/web/modules/admin-tools/frontend/js/user-list/context/user-list-context.tsx b/services/web/modules/admin-tools/frontend/js/user-list/context/user-list-context.tsx
index e64a74db14..dd7045b22d 100644
--- a/services/web/modules/admin-tools/frontend/js/user-list/context/user-list-context.tsx
+++ b/services/web/modules/admin-tools/frontend/js/user-list/context/user-list-context.tsx
@@ -20,7 +20,7 @@ import {
User,
Sort,
} from '../../../../types/user/api'
-import { getUsers } from '../util/api'
+import { getUsers, searchUsers } from '../util/api'
import sortUsers from '../util/sort-users'
import { UserIdentityProvider } from './user-identity-context'
@@ -131,6 +131,7 @@ export function UserListProvider({ children }: UserListProviderProps) {
)
const [searchText, setSearchText] = useState('')
+ const [searchResults, setSearchResults] = useState(null)
const {
isLoading: loading,
@@ -157,26 +158,42 @@ export function UserListProvider({ children }: UserListProviderProps) {
})
}, [prefetchedUsersBlob, runAsync])
+ useEffect(() => {
+ if (!searchText.length) {
+ setSearchResults(null)
+ return
+ }
+ const abortController = new AbortController()
+ const timer = setTimeout(() => {
+ searchUsers(searchText, abortController.signal)
+ .then(data => {
+ setSearchResults(data.users)
+ })
+ .catch(error => {
+ if (error.name === 'AbortError') {
+ debugConsole.log('Search aborted')
+ return
+ }
+ debugConsole.error('Error searching users:', error)
+ setSearchResults(null)
+ })
+ }, 300) // 300ms debounce
+
+ return () => {
+ clearTimeout(timer)
+ abortController.abort()
+ }
+ }, [searchText])
+
const addUserToView = useCallback((newUser: Partial) => {
setLoadedUsers(prev => [newUser as User, ...prev])
}, [])
const processedUsers = useMemo(() => {
- let users = loadedUsers
-
- if (searchText.length) {
- const searchTextLowerCase = searchText.toLowerCase()
- users = users.filter(user =>
- user.email?.toLowerCase().includes(searchTextLowerCase) ||
- user.firstName?.toLowerCase().includes(searchTextLowerCase) ||
- user.lastName?.toLowerCase().includes(searchTextLowerCase)
- )
- }
-
+ let users = searchResults !== null ? searchResults : loadedUsers
users = arrayFilter(users, filters[filter])
-
return sortUsers(users, sort)
- }, [loadedUsers, searchText, filter, sort])
+ }, [searchResults, loadedUsers, filter, sort])
const visibleUsers = useMemo(() => {
return processedUsers.slice(0, maxVisibleUsers)
@@ -228,8 +245,8 @@ export function UserListProvider({ children }: UserListProviderProps) {
)
const selectedUsers = useMemo(() => {
- return loadedUsers.filter(user => selectedUserIds.has(user.id))
- }, [selectedUserIds, loadedUsers])
+ return processedUsers.filter(user => selectedUserIds.has(user.id))
+ }, [selectedUserIds, processedUsers])
const selectOrUnselectAllUsers = useCallback(
(checked: any) => {
diff --git a/services/web/modules/admin-tools/frontend/js/user-list/util/api.ts b/services/web/modules/admin-tools/frontend/js/user-list/util/api.ts
index 033ae3a06e..5a28b0d99a 100644
--- a/services/web/modules/admin-tools/frontend/js/user-list/util/api.ts
+++ b/services/web/modules/admin-tools/frontend/js/user-list/util/api.ts
@@ -5,6 +5,10 @@ export function getUsers(sortBy: Sort): Promise {
return postJSON('/admin/users', { body: { sort: sortBy } })
}
+export function searchUsers(search: string, signal?: AbortSignal): Promise {
+ return postJSON('/admin/users/search', { body: { search }, signal })
+}
+
export function updateUser(userId: string, userData: Partial) {
return postJSON(`/admin/user/${userId}/update`, { body: userData })
}
diff --git a/services/web/modules/admin-tools/frontend/stylesheets/user-list.scss b/services/web/modules/admin-tools/frontend/stylesheets/user-list.scss
index 4babc4c2c0..8d599bf1ef 100644
--- a/services/web/modules/admin-tools/frontend/stylesheets/user-list.scss
+++ b/services/web/modules/admin-tools/frontend/stylesheets/user-list.scss
@@ -610,5 +610,6 @@ form.user-search {
}
}
-
-
\ No newline at end of file
+.tab-content {
+ margin-top: var(--spacing-05) !important;
+}
\ No newline at end of file
diff --git a/services/web/modules/admin-tools/types/user/api.d.ts b/services/web/modules/admin-tools/types/user/api.d.ts
index 46abb9bedb..b20bc64fda 100644
--- a/services/web/modules/admin-tools/types/user/api.d.ts
+++ b/services/web/modules/admin-tools/types/user/api.d.ts
@@ -47,6 +47,10 @@ export type UserApi = {
inactive: boolean
deleted?: boolean
deletedAt?: Date
+ features?: {
+ collaborators?: number
+ compileTimeout?: number
+ }
}
export type User = MergeAndOverride<