From 662614de0d6643a7099a15819366bf6357b3569c Mon Sep 17 00:00:00 2001 From: JimmFly Date: Tue, 13 May 2025 02:43:01 +0000 Subject: [PATCH] feat(admin): create user with password (#12112) close AF-2494 ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Added support for importing users with optional passwords via CSV, including password validation and error handling. - Enhanced user creation form to allow password input and validation according to server-configured requirements. - User table now displays a password column and provides granular error highlighting for email and password fields. - Introduced detailed CSV format guidance and improved import dialog with dynamic content and footer controls. - **Improvements** - Updated import dialog workflow for clearer CSV formatting guidance and improved user feedback during import. - Refined dropdown menu actions and improved UI clarity for user management actions. - **Bug Fixes** - Corrected menu action handlers for "Edit" and "Reset Password" in user actions dropdown. - **Chores** - Refactored import dialog code into modular components for maintainability and clarity. --- .../components/data-table-row-actions.tsx | 25 +- .../components/data-table-toolbar.tsx | 2 +- .../components/import-users-dialog.tsx | 314 ------------------ .../import-users/csv-format-guidance.tsx | 51 +++ .../{ => import-users}/file-upload-area.tsx | 0 .../import-users/import-content.tsx | 73 ++++ .../components/import-users/import-footer.tsx | 168 ++++++++++ .../components/import-users/index.tsx | 109 ++++++ .../import-users/use-import-users-state.ts | 193 +++++++++++ .../components/use-user-management.ts | 3 +- .../modules/accounts/components/user-form.tsx | 63 +++- .../accounts/components/user-table.tsx | 23 +- .../admin/src/modules/accounts/schema.ts | 1 + .../src/modules/accounts/utils/csv-utils.ts | 75 ++++- 14 files changed, 764 insertions(+), 336 deletions(-) delete mode 100644 packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx create mode 100644 packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx rename packages/frontend/admin/src/modules/accounts/components/{ => import-users}/file-upload-area.tsx (100%) create mode 100644 packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx create mode 100644 packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx create mode 100644 packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx create mode 100644 packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx index a71c13bd25..e6ae064e86 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx @@ -170,24 +170,27 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { - - Reset Password - - Edit + + Edit + + + + {user.hasPassword ? 'Reset Password' : 'Setup Account'} {user.disabled && ( - Enable Email + + Enable Email )} @@ -196,14 +199,16 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) { className="px-2 py-[6px] text-sm font-normal gap-2 text-red-500 cursor-pointer focus:text-red-500" onSelect={openDisableDialog} > - Disable & Delete data + + Disable & Delete data )} - Delete + + Delete diff --git a/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx b/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx index 82a9b2d0aa..fb49b6e6c2 100644 --- a/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx @@ -17,7 +17,7 @@ import { useRightPanel } from '../../panel/context'; import type { UserType } from '../schema'; import { DiscardChanges } from './discard-changes'; import { ExportUsersDialog } from './export-users-dialog'; -import { ImportUsersDialog } from './import-users-dialog'; +import { ImportUsersDialog } from './import-users'; import { CreateUserForm } from './user-form'; interface DataTableToolbarProps { diff --git a/packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx b/packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx deleted file mode 100644 index 97e6c818cb..0000000000 --- a/packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx +++ /dev/null @@ -1,314 +0,0 @@ -import { Button } from '@affine/admin/components/ui/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@affine/admin/components/ui/dialog'; -import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { toast } from 'sonner'; - -import { - downloadCsvTemplate, - exportImportResults, - getValidUsersToImport, - ImportStatus, - type ParsedUser, - processCSVFile, -} from '../utils/csv-utils'; -import { FileUploadArea, type FileUploadAreaRef } from './file-upload-area'; -import { - useImportUsers, - type UserImportReturnType, -} from './use-user-management'; -import { UserTable } from './user-table'; - -interface ImportUsersDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function ImportUsersDialog({ - open, - onOpenChange, -}: ImportUsersDialogProps) { - const [isImporting, setIsImporting] = useState(false); - const [parsedUsers, setParsedUsers] = useState([]); - const [isPreviewMode, setIsPreviewMode] = useState(false); - const [isFormatError, setIsFormatError] = useState(false); - const importUsers = useImportUsers(); - const fileUploadRef = useRef(null); - - const handleUpload = useCallback( - () => fileUploadRef.current?.triggerFileUpload(), - [] - ); - - // Reset all states when dialog is closed - useEffect(() => { - if (open) { - setIsPreviewMode(false); - setParsedUsers([]); - setIsImporting(false); - setIsFormatError(false); - } - }, [open]); - - const importUsersCallback = useCallback( - (result: UserImportReturnType) => { - const successfulUsers = result.filter( - (user): user is Extract => - user.__typename === 'UserType' - ); - - const failedUsers = result.filter( - ( - user - ): user is Extract< - typeof user, - { __typename: 'UserImportFailedType' } - > => user.__typename === 'UserImportFailedType' - ); - - const successCount = successfulUsers.length; - const failedCount = parsedUsers.length - successCount; - - if (failedCount > 0) { - toast.info( - `Successfully imported ${successCount} users, ${failedCount} failed` - ); - } else { - toast.success(`Successfully imported ${successCount} users`); - } - - const successfulUserEmails = new Set( - successfulUsers.map(user => user.email) - ); - - const failedUserErrorMap = new Map( - failedUsers.map(user => [user.email, user.error]) - ); - - setParsedUsers(prev => { - return prev.map(user => { - if (successfulUserEmails.has(user.email)) { - return { - ...user, - importStatus: ImportStatus.Success, - }; - } - - const errorMessage = failedUserErrorMap.get(user.email) || user.error; - return { - ...user, - importStatus: ImportStatus.Failed, - importError: errorMessage, - }; - }); - }); - - setIsImporting(false); - }, - [parsedUsers.length, setIsImporting] - ); - - const handleFileSelected = useCallback(async (file: File) => { - setIsImporting(true); - try { - await processCSVFile( - file, - validatedUsers => { - setParsedUsers(validatedUsers); - setIsPreviewMode(true); - setIsImporting(false); - }, - () => { - setIsImporting(false); - setIsFormatError(true); - } - ); - } catch (error) { - console.error('Failed to process file', error); - setIsImporting(false); - setIsFormatError(true); - } - }, []); - - const confirmImport = useAsyncCallback(async () => { - setIsImporting(true); - try { - const validUsersToImport = getValidUsersToImport(parsedUsers); - - setParsedUsers(prev => - prev.map(user => - user.valid ? { ...user, importStatus: ImportStatus.Processing } : user - ) - ); - - await importUsers({ users: validUsersToImport }, importUsersCallback); - // Note: setIsImporting(false) is now handled in importUsersCallback - } catch (error) { - console.error('Failed to import users', error); - toast.error('Failed to import users'); - setIsImporting(false); - } - }, [importUsers, importUsersCallback, parsedUsers]); - - const cancelImport = useCallback(() => { - setIsPreviewMode(false); - setParsedUsers([]); - }, []); - - const resetFormatError = useCallback(() => { - setIsFormatError(false); - }, []); - - // Handle closing the dialog after import is complete - const handleDone = useCallback(() => { - // Reset all states and close the dialog - setIsPreviewMode(false); - setParsedUsers([]); - setIsImporting(false); - setIsFormatError(false); - onOpenChange(false); - }, [onOpenChange]); - - // Export failed imports to CSV - const exportResult = useCallback(() => { - exportImportResults(parsedUsers); - }, [parsedUsers]); - - const isImported = useMemo(() => { - return parsedUsers.some( - user => user.importStatus && user.importStatus !== ImportStatus.Processing - ); - }, [parsedUsers]); - - const handleConfirm = useCallback(() => { - if (isImported) { - exportResult(); - handleDone(); - } else { - confirmImport(); - } - }, [confirmImport, exportResult, handleDone, isImported]); - - return ( - - - - - {isFormatError - ? 'Incorrect import format' - : isPreviewMode - ? isImported - ? 'Import results' - : 'Confirm import' - : 'Import'} - - - - {isFormatError ? ( - 'You need to import the accounts by importing a CSV file in the correct format. Please download the CSV template.' - ) : isPreviewMode ? ( -
- {isImported ? null : ( -

- {parsedUsers.length} users detected from the CSV file. Please - confirm the user list below and import. -

- )} - -
- ) : ( -
-

- You need to import the accounts by importing a CSV file in the - correct format. Please download the CSV template. -

- -
- )} -
- - - {isFormatError ? ( - <> -
- CSV template -
- - - ) : isPreviewMode ? ( - <> - - - - ) : ( - <> -
- CSV template -
- - - )} -
-
-
- ); -} diff --git a/packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx b/packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx new file mode 100644 index 0000000000..63e620d905 --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx @@ -0,0 +1,51 @@ +import { WarningIcon } from '@blocksuite/icons/rc'; +import { cssVar } from '@toeverything/theme'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import type { FC } from 'react'; + +interface CsvFormatGuidanceProps { + passwordLimits: { + minLength: number; + maxLength: number; + }; +} + +/** + * Component that displays CSV format guidelines + */ +export const CsvFormatGuidance: FC = ({ + passwordLimits, +}) => { + return ( +
+
+ +
+
+

CSV file includes username, email, and password.

+
    + {[ + `Username (optional): any text.`, + `Email (required): e.g., user@example.com.`, + `Password (optional): ${passwordLimits.minLength}–${passwordLimits.maxLength} characters.`, + ].map((text, index) => ( +
  • + + {text} +
  • + ))} +
+
+
+ ); +}; diff --git a/packages/frontend/admin/src/modules/accounts/components/file-upload-area.tsx b/packages/frontend/admin/src/modules/accounts/components/import-users/file-upload-area.tsx similarity index 100% rename from packages/frontend/admin/src/modules/accounts/components/file-upload-area.tsx rename to packages/frontend/admin/src/modules/accounts/components/import-users/file-upload-area.tsx diff --git a/packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx b/packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx new file mode 100644 index 0000000000..455cf38763 --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx @@ -0,0 +1,73 @@ +import type { FC, RefObject } from 'react'; + +import type { ParsedUser } from '../../utils/csv-utils'; +import { UserTable } from '../user-table'; +import { CsvFormatGuidance } from './csv-format-guidance'; +import { FileUploadArea, type FileUploadAreaRef } from './file-upload-area'; + +interface ImportPreviewContentProps { + parsedUsers: ParsedUser[]; + isImported: boolean; +} + +/** + * Component for the preview mode content + */ +export const ImportPreviewContent: FC = ({ + parsedUsers, + isImported, +}) => { + return ( +
+ {!isImported && ( +

+ {parsedUsers.length} users detected from the CSV file. Please confirm + the user list below and import. +

+ )} + +
+ ); +}; + +interface ImportInitialContentProps { + passwordLimits: { + minLength: number; + maxLength: number; + }; + fileUploadRef: RefObject; + onFileSelected: (file: File) => Promise; +} + +/** + * Component for the initial import screen + */ +export const ImportInitialContent: FC = ({ + passwordLimits, + fileUploadRef, + onFileSelected, +}) => { + return ( +
+

+ You need to import the accounts by importing a CSV file in the correct + format. Please download the CSV template. +

+ + +
+ ); +}; + +interface ImportErrorContentProps { + message?: string; +} + +/** + * Component for displaying import errors + */ +export const ImportErrorContent: FC = ({ + message = 'You need to import the accounts by importing a CSV file in the correct format. Please download the CSV template.', +}) => { + return message; +}; diff --git a/packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx b/packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx new file mode 100644 index 0000000000..ff8101d1a0 --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx @@ -0,0 +1,168 @@ +import { Button } from '@affine/admin/components/ui/button'; +import { DialogFooter } from '@affine/admin/components/ui/dialog'; +import type { FC } from 'react'; + +import { downloadCsvTemplate, ImportStatus } from '../../utils/csv-utils'; + +interface ImportUsersFooterProps { + isFormatError: boolean; + isPreviewMode: boolean; + isImporting: boolean; + isImported: boolean; + resetFormatError: () => void; + cancelImport: () => void; + handleConfirm: () => void; + handleUpload: () => void; + parsedUsers: { + importStatus?: ImportStatus; + }[]; +} + +/** + * Component for the dialog footer with appropriate buttons based on the import state + */ +export const ImportUsersFooter: FC = ({ + isFormatError, + isPreviewMode, + isImporting, + isImported, + resetFormatError, + cancelImport, + handleConfirm, + handleUpload, + parsedUsers, +}) => { + return ( + + {isFormatError ? ( + + ) : isPreviewMode ? ( + + ) : ( + + )} + + ); +}; + +interface FormatErrorFooterProps { + downloadCsvTemplate: () => void; + resetFormatError: () => void; +} + +/** + * Footer component when there's a format error + */ +const FormatErrorFooter: FC = ({ + downloadCsvTemplate, + resetFormatError, +}) => ( + <> +
+ CSV template +
+ + +); + +interface PreviewModeFooterProps { + isImporting: boolean; + isImported: boolean; + cancelImport: () => void; + handleConfirm: () => void; + parsedUsers: { + importStatus?: ImportStatus; + }[]; +} + +/** + * Footer component for preview mode + */ +const PreviewModeFooter: FC = ({ + isImporting, + isImported, + cancelImport, + handleConfirm, + parsedUsers, +}) => ( + <> + + + +); + +interface InitialFooterProps { + isImporting: boolean; + downloadCsvTemplate: () => void; + handleUpload: () => void; +} + +/** + * Footer component for initial state + */ +const InitialFooter: FC = ({ + isImporting, + downloadCsvTemplate, + handleUpload, +}) => ( + <> +
+ CSV template +
+ + +); diff --git a/packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx b/packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx new file mode 100644 index 0000000000..f63302b347 --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx @@ -0,0 +1,109 @@ +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@affine/admin/components/ui/dialog'; +import { useEffect, useRef } from 'react'; + +import { useServerConfig } from '../../../common'; +import type { FileUploadAreaRef } from './file-upload-area'; +import { + ImportErrorContent, + ImportInitialContent, + ImportPreviewContent, +} from './import-content'; +import { ImportUsersFooter } from './import-footer'; +import { useImportUsersState } from './use-import-users-state'; + +export interface ImportUsersDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +/** + * Dialog for importing users from a CSV file + */ +export function ImportUsersDialog({ + open, + onOpenChange, +}: ImportUsersDialogProps) { + const fileUploadRef = useRef(null); + const serverConfig = useServerConfig(); + const passwordLimits = serverConfig.credentialsRequirement.password; + + const handleUpload = () => fileUploadRef.current?.triggerFileUpload(); + + const { + isImporting, + parsedUsers, + isPreviewMode, + isFormatError, + isImported, + handleFileSelected, + cancelImport, + resetFormatError, + handleConfirm, + resetState, + } = useImportUsersState({ + passwordLimits, + onClose: () => onOpenChange(false), + }); + + // Reset all states when dialog is opened + useEffect(() => { + if (open) { + resetState(); + } + }, [open, resetState]); + + return ( + + + + + {isFormatError + ? 'Incorrect import format' + : isPreviewMode + ? isImported + ? 'Import results' + : 'Confirm import' + : 'Import'} + + +
+ {isFormatError ? ( + + ) : isPreviewMode ? ( + + ) : ( + + )} +
+ + +
+
+ ); +} diff --git a/packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts b/packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts new file mode 100644 index 0000000000..1a224dbb9c --- /dev/null +++ b/packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts @@ -0,0 +1,193 @@ +import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; +import { useCallback, useState } from 'react'; +import { toast } from 'sonner'; + +import { + exportImportResults, + getValidUsersToImport, + ImportStatus, + type ParsedUser, + processCSVFile, + validateUsers, +} from '../../utils/csv-utils'; +import { + useImportUsers, + type UserImportReturnType, +} from '../use-user-management'; + +export interface ImportUsersStateProps { + passwordLimits: { + minLength: number; + maxLength: number; + }; + onClose: () => void; +} + +export function useImportUsersState({ + passwordLimits, + onClose, +}: ImportUsersStateProps) { + const [isImporting, setIsImporting] = useState(false); + const [parsedUsers, setParsedUsers] = useState([]); + const [isPreviewMode, setIsPreviewMode] = useState(false); + const [isFormatError, setIsFormatError] = useState(false); + const importUsers = useImportUsers(); + + // Reset all states when dialog is closed + const resetState = useCallback(() => { + setIsPreviewMode(false); + setParsedUsers([]); + setIsImporting(false); + setIsFormatError(false); + }, []); + + const importUsersCallback = useCallback( + (result: UserImportReturnType) => { + const successfulUsers = result.filter( + (user): user is Extract => + user.__typename === 'UserType' + ); + + const failedUsers = result.filter( + ( + user + ): user is Extract< + typeof user, + { __typename: 'UserImportFailedType' } + > => user.__typename === 'UserImportFailedType' + ); + + const successCount = successfulUsers.length; + const failedCount = parsedUsers.length - successCount; + + if (failedCount > 0) { + toast.info( + `Successfully imported ${successCount} users, ${failedCount} failed` + ); + } else { + toast.success(`Successfully imported ${successCount} users`); + } + + const successfulUserEmails = new Set( + successfulUsers.map(user => user.email) + ); + + const failedUserErrorMap = new Map( + failedUsers.map(user => [user.email, user.error]) + ); + + setParsedUsers(prev => { + return prev.map(user => { + if (successfulUserEmails.has(user.email)) { + return { + ...user, + importStatus: ImportStatus.Success, + }; + } + + const errorMessage = failedUserErrorMap.get(user.email) || user.error; + return { + ...user, + importStatus: ImportStatus.Failed, + importError: errorMessage, + }; + }); + }); + + setIsImporting(false); + }, + [parsedUsers.length] + ); + + const handleFileSelected = useCallback( + async (file: File) => { + setIsImporting(true); + try { + await processCSVFile( + file, + usersFromCsv => { + const validatedUsers = validateUsers(usersFromCsv, passwordLimits); + setParsedUsers(validatedUsers); + setIsPreviewMode(true); + setIsImporting(false); + }, + () => { + setIsImporting(false); + setIsFormatError(true); + } + ); + } catch (error) { + console.error('Failed to process file', error); + setIsImporting(false); + setIsFormatError(true); + } + }, + [passwordLimits] + ); + + const confirmImport = useAsyncCallback(async () => { + setIsImporting(true); + try { + const validUsersToImport = getValidUsersToImport(parsedUsers); + + setParsedUsers(prev => + prev.map(user => ({ + ...user, + importStatus: ImportStatus.Processing, + })) + ); + + await importUsers({ users: validUsersToImport }, importUsersCallback); + } catch (error) { + console.error('Failed to import users', error); + toast.error('Failed to import users'); + setIsImporting(false); + } + }, [importUsers, importUsersCallback, parsedUsers]); + + const cancelImport = useCallback(() => { + setIsPreviewMode(false); + setParsedUsers([]); + }, []); + + const resetFormatError = useCallback(() => { + setIsFormatError(false); + }, []); + + // Handle closing the dialog after import is complete + const handleDone = useCallback(() => { + resetState(); + onClose(); + }, [onClose, resetState]); + + // Export failed imports to CSV + const exportResult = useCallback(() => { + exportImportResults(parsedUsers); + }, [parsedUsers]); + + const isImported = !!parsedUsers.some( + user => user.importStatus && user.importStatus !== ImportStatus.Processing + ); + + const handleConfirm = useCallback(() => { + if (isImported) { + exportResult(); + handleDone(); + } else { + confirmImport(); + } + }, [confirmImport, exportResult, handleDone, isImported]); + + return { + isImporting, + parsedUsers, + isPreviewMode, + isFormatError, + isImported, + handleFileSelected, + cancelImport, + resetFormatError, + handleConfirm, + resetState, + }; +} diff --git a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts index fddf2e00fc..15726d8528 100644 --- a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts +++ b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts @@ -47,12 +47,13 @@ export const useCreateUser = () => { const revalidate = useMutateQueryResource(); const create = useAsyncCallback( - async ({ name, email, features }: UserInput) => { + async ({ name, email, password, features }: UserInput) => { try { const account = await createAccount({ input: { name, email, + password: password === '' ? undefined : password, }, }); diff --git a/packages/frontend/admin/src/modules/accounts/components/user-form.tsx b/packages/frontend/admin/src/modules/accounts/components/user-form.tsx index f6d7f36995..5b0aa079e1 100644 --- a/packages/frontend/admin/src/modules/accounts/components/user-form.tsx +++ b/packages/frontend/admin/src/modules/accounts/components/user-form.tsx @@ -4,13 +4,16 @@ import { Label } from '@affine/admin/components/ui/label'; import { Separator } from '@affine/admin/components/ui/separator'; import { Switch } from '@affine/admin/components/ui/switch'; import type { FeatureType } from '@affine/graphql'; +import { cssVarV2 } from '@toeverything/theme/v2'; import { ChevronRightIcon } from 'lucide-react'; import type { ChangeEvent } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { useServerConfig } from '../../common'; import { RightPanelHeader } from '../../header'; import type { UserInput, UserType } from '../schema'; +import { validateEmails, validatePassword } from '../utils/csv-utils'; import { useCreateUser, useUpdateUser } from './use-user-management'; type UserFormProps = { @@ -20,6 +23,7 @@ type UserFormProps = { onConfirm: (user: UserInput) => void; onValidate: (user: Partial) => boolean; actions?: React.ReactNode; + showOption?: boolean; }; function UserForm({ @@ -29,6 +33,7 @@ function UserForm({ onConfirm, onValidate, actions, + showOption, }: UserFormProps) { const serverConfig = useServerConfig(); @@ -36,9 +41,10 @@ function UserForm({ () => ({ name: defaultValue?.name ?? '', email: defaultValue?.email ?? '', + password: defaultValue?.password ?? '', features: defaultValue?.features ?? [], }), - [defaultValue?.email, defaultValue?.features, defaultValue?.name] + [defaultValue] ); const [changes, setChanges] = useState>(defaultUser); @@ -68,7 +74,8 @@ function UserForm({ // @ts-expect-error checked onConfirm(changes); - }, [canSave, changes, onConfirm]); + setChanges(defaultUser); + }, [canSave, changes, defaultUser, onConfirm]); const onFeatureChanged = useCallback( (feature: FeatureType, checked: boolean) => { @@ -116,6 +123,19 @@ function UserForm({ onChange={setField} placeholder="Enter email address" /> + {showOption && ( + <> + + + + )}
@@ -167,12 +187,14 @@ function ToggleItem({ function InputItem({ label, field, + optional, value, onChange, placeholder, }: { label: string; field: keyof UserInput; + optional?: boolean; value?: string; onChange: (field: keyof UserInput, value: string) => void; placeholder?: string; @@ -185,9 +207,20 @@ function InputItem({ ); return ( -
-