Repository: toeverything/AFFiNE
Pull request #12112: feat(admin): create user with password
Author: login: JimmFly
Created: 2025-05-02T09:51:31Z | Merged: 2025-05-13T02:58:24Z

close AF-2494

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
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 a71c13bd25156..e6ae064e866ba 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
@@ -171,23 +171,26 @@ export function DataTableRowActions({ user }: DataTableRowActionsProps) {
         </DropdownMenuTrigger>
         <DropdownMenuContent align="end" className="w-[214px] p-[5px] gap-2">
           <DropdownMenuItem
+            onSelect={handleEdit}
             className="px-2 py-[6px] text-sm font-normal gap-2 cursor-pointer"
-            onSelect={openResetPasswordDialog}
           >
-            <LockIcon fontSize={20} /> Reset Password
+            <EditIcon fontSize={20} />
+            Edit
           </DropdownMenuItem>
           <DropdownMenuItem
-            onSelect={handleEdit}
             className="px-2 py-[6px] text-sm font-normal gap-2 cursor-pointer"
+            onSelect={openResetPasswordDialog}
           >
-            <EditIcon fontSize={20} /> Edit
+            <LockIcon fontSize={20} />
+            {user.hasPassword ? 'Reset Password' : 'Setup Account'}
           </DropdownMenuItem>
           {user.disabled && (
             <DropdownMenuItem
               className="px-2 py-[6px] text-sm font-normal gap-2 cursor-pointer"
               onSelect={openEnableDialog}
             >
-              <AccountBanIcon fontSize={20} /> Enable Email
+              <AccountBanIcon fontSize={20} />
+              Enable Email
             </DropdownMenuItem>
           )}
           <DropdownMenuSeparator />
@@ -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}
             >
-              <AccountBanIcon fontSize={20} /> Disable & Delete data
+              <AccountBanIcon fontSize={20} />
+              Disable & Delete data
             </DropdownMenuItem>
           )}
           <DropdownMenuItem
             className="px-2 py-[6px] text-sm font-normal gap-2 text-red-500 cursor-pointer focus:text-red-500"
             onSelect={openDeleteDialog}
           >
-            <DeleteIcon fontSize={20} /> Delete
+            <DeleteIcon fontSize={20} />
+            Delete
           </DropdownMenuItem>
         </DropdownMenuContent>
       </DropdownMenu>
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 82a9b2d0aa92d..fb49b6e6c2ad6 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<TData> {
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 97e6c818cb5aa..0000000000000
--- 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<ParsedUser[]>([]);
-  const [isPreviewMode, setIsPreviewMode] = useState(false);
-  const [isFormatError, setIsFormatError] = useState(false);
-  const importUsers = useImportUsers();
-  const fileUploadRef = useRef<FileUploadAreaRef>(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<typeof user, { __typename: 'UserType' }> =>
-          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 (
-    <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent
-        className={
-          isPreviewMode ? 'sm:max-w-[600px] flex-col' : 'sm:max-w-[425px]'
-        }
-      >
-        <DialogHeader>
-          <DialogTitle>
-            {isFormatError
-              ? 'Incorrect import format'
-              : isPreviewMode
-                ? isImported
-                  ? 'Import results'
-                  : 'Confirm import'
-                : 'Import'}
-          </DialogTitle>
-        </DialogHeader>
-        <DialogDescription className="text-[15px] mt-3">
-          {isFormatError ? (
-            'You need to import the accounts by importing a CSV file in the correct format. Please download the CSV template.'
-          ) : isPreviewMode ? (
-            <div className="grid gap-3">
-              {isImported ? null : (
-                <p>
-                  {parsedUsers.length} users detected from the CSV file. Please
-                  confirm the user list below and import.
-                </p>
-              )}
-              <UserTable users={parsedUsers} />
-            </div>
-          ) : (
-            <div className="grid gap-3">
-              <p>
-                You need to import the accounts by importing a CSV file in the
-                correct format. Please download the CSV template.
-              </p>
-              <FileUploadArea
-                ref={fileUploadRef}
-                onFileSelected={handleFileSelected}
-              />
-            </div>
-          )}
-        </DialogDescription>
-
-        <DialogFooter
-          className={`flex-col mt-6 sm:flex-row sm:justify-between items-center ${isPreviewMode ? 'sm:justify-end' : 'sm:justify-between'}`}
-        >
-          {isFormatError ? (
-            <>
-              <div
-                onClick={downloadCsvTemplate}
-                className="mb-2 sm:mb-0 text-[15px] px-0 py-2 h-10 underline cursor-pointer"
-              >
-                CSV template
-              </div>
-              <Button
-                type="button"
-                variant="outline"
-                onClick={resetFormatError}
-                className="w-full sm:w-auto text-[15px] px-4 py-2 h-10"
-              >
-                Done
-              </Button>
-            </>
-          ) : isPreviewMode ? (
-            <>
-              <Button
-                variant="outline"
-                onClick={cancelImport}
-                className="w-full mb-2 sm:mb-0 sm:w-auto"
-                disabled={isImporting}
-              >
-                Cancel
-              </Button>
-              <Button
-                type="button"
-                onClick={handleConfirm}
-                className="w-full sm:w-auto text-[15px] px-4 py-2 h-10"
-                disabled={
-                  isImporting ||
-                  parsedUsers.some(
-                    user => user.importStatus === ImportStatus.Processing
-                  )
-                }
-              >
-                {isImporting
-                  ? 'Importing...'
-                  : isImported
-                    ? 'Export'
-                    : 'Confirm Import'}
-              </Button>
-            </>
-          ) : (
-            <>
-              <div
-                onClick={downloadCsvTemplate}
-                className="mb-2 sm:mb-0 underline text-[15px] cursor-pointer"
-              >
-                CSV template
-              </div>
-              <Button
-                type="button"
-                onClick={handleUpload}
-                className="w-full sm:w-auto text-[15px] px-4 py-2 h-10"
-                disabled={isImporting}
-              >
-                {isImporting ? 'Parsing...' : 'Choose a file'}
-              </Button>
-            </>
-          )}
-        </DialogFooter>
-      </DialogContent>
-    </Dialog>
-  );
-}
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 0000000000000..63e620d90518b
--- /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<CsvFormatGuidanceProps> = ({
+  passwordLimits,
+}) => {
+  return (
+    <div
+      className="flex p-1.5 gap-1"
+      style={{
+        fontSize: cssVar('fontXs'),
+        color: cssVarV2('text/secondary'),
+        backgroundColor: cssVarV2('layer/background/secondary'),
+      }}
+    >
+      <div className="flex justify-center py-0.5">
+        <WarningIcon fontSize={16} color={cssVarV2('icon/primary')} />
+      </div>
+      <div>
+        <p>CSV file includes username, email, and password.</p>
+        <ul>
+          {[
+            `Username (optional): any text.`,
+            `Email (required): e.g., user@example.com.`,
+            `Password (optional): ${passwordLimits.minLength}–${passwordLimits.maxLength} characters.`,
+          ].map((text, index) => (
+            <li
+              key={`guidance-${index}`}
+              className="relative pl-2 leading-normal"
+            >
+              <span className="absolute left-0 top-2 w-1 h-1 rounded-full bg-current" />
+              {text}
+            </li>
+          ))}
+        </ul>
+      </div>
+    </div>
+  );
+};
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 0000000000000..455cf38763abb
--- /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<ImportPreviewContentProps> = ({
+  parsedUsers,
+  isImported,
+}) => {
+  return (
+    <div className="grid gap-3">
+      {!isImported && (
+        <p>
+          {parsedUsers.length} users detected from the CSV file. Please confirm
+          the user list below and import.
+        </p>
+      )}
+      <UserTable users={parsedUsers} />
+    </div>
+  );
+};
+
+interface ImportInitialContentProps {
+  passwordLimits: {
+    minLength: number;
+    maxLength: number;
+  };
+  fileUploadRef: RefObject<FileUploadAreaRef | null>;
+  onFileSelected: (file: File) => Promise<void>;
+}
+
+/**
+ * Component for the initial import screen
+ */
+export const ImportInitialContent: FC<ImportInitialContentProps> = ({
+  passwordLimits,
+  fileUploadRef,
+  onFileSelected,
+}) => {
+  return (
+    <div className="grid gap-3">
+      <p>
+        You need to import the accounts by importing a CSV file in the correct
+        format. Please download the CSV template.
+      </p>
+      <CsvFormatGuidance passwordLimits={passwordLimits} />
+      <FileUploadArea ref={fileUploadRef} onFileSelected={onFileSelected} />
+    </div>
+  );
+};
+
+interface ImportErrorContentProps {
+  message?: string;
+}
+
+/**
+ * Component for displaying import errors
+ */
+export const ImportErrorContent: FC<ImportErrorContentProps> = ({
+  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 0000000000000..ff8101d1a003f
--- /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<ImportUsersFooterProps> = ({
+  isFormatError,
+  isPreviewMode,
+  isImporting,
+  isImported,
+  resetFormatError,
+  cancelImport,
+  handleConfirm,
+  handleUpload,
+  parsedUsers,
+}) => {
+  return (
+    <DialogFooter
+      className={`flex-col mt-6 sm:flex-row sm:justify-between items-center ${
+        isPreviewMode ? 'sm:justify-end' : 'sm:justify-between'
+      }`}
+    >
+      {isFormatError ? (
+        <FormatErrorFooter
+          downloadCsvTemplate={downloadCsvTemplate}
+          resetFormatError={resetFormatError}
+        />
+      ) : isPreviewMode ? (
+        <PreviewModeFooter
+          isImporting={isImporting}
+          isImported={isImported}
+          cancelImport={cancelImport}
+          handleConfirm={handleConfirm}
+          parsedUsers={parsedUsers}
+        />
+      ) : (
+        <InitialFooter
+          isImporting={isImporting}
+          downloadCsvTemplate={downloadCsvTemplate}
+          handleUpload={handleUpload}
+        />
+      )}
+    </DialogFooter>
+  );
+};
+
+interface FormatErrorFooterProps {
+  downloadCsvTemplate: () => void;
+  resetFormatError: () => void;
+}
+
+/**
+ * Footer component when there's a format error
+ */
+const FormatErrorFooter: FC<FormatErrorFooterProps> = ({
+  downloadCsvTemplate,
+  resetFormatError,
+}) => (
+  <>
+    <div
+      onClick={downloadCsvTemplate}
+      className="mb-2 sm:mb-0 text-[15px] px-0 py-2 h-10 underline cursor-pointer"
+    >
+      CSV template
+    </div>
+    <Button
+      type="button"
+      variant="outline"
+      onClick={resetFormatError}
+      className="w-full sm:w-auto text-[15px] px-4 py-2 h-10"
+    >
+      Done
+    </Button>
+  </>
+);
+
+interface PreviewModeFooterProps {
+  isImporting: boolean;
+  isImported: boolean;
+  cancelImport: () => void;
+  handleConfirm: () => void;
+  parsedUsers: {
+    importStatus?: ImportStatus;
+  }[];
+}
+
+/**
+ * Footer component for preview mode
+ */
+const PreviewModeFooter: FC<PreviewModeFooterProps> = ({
+  isImporting,
+  isImported,
+  cancelImport,
+  handleConfirm,
+  parsedUsers,
+}) => (
+  <>
+    <Button
+      variant="outline"
+      onClick={cancelImport}
+      className="w-full mb-2 sm:mb-0 sm:w-auto"
+      disabled={isImporting}
+    >
+      Cancel
+    </Button>
+    <Button
+      type="button"
+      onClick={handleConfirm}
+      className="w-full sm:w-auto text-[15px] px-4 py-2 h-10"
+      disabled={
+        isImporting ||
+        parsedUsers.some(user => user.importStatus === ImportStatus.Processing)
+      }
+    >
+      {isImporting ? 'Importing...' : isImported ? 'Export' : 'Confirm Import'}
+    </Button>
+  </>
+);
+
+interface InitialFooterProps {
+  isImporting: boolean;
+  downloadCsvTemplate: () => void;
+  handleUpload: () => void;
+}
+
+/**
+ * Footer component for initial state
+ */
+const InitialFooter: FC<InitialFooterProps> = ({
+  isImporting,
+  downloadCsvTemplate,
+  handleUpload,
+}) => (
+  <>
+    <div
+      onClick={downloadCsvTemplate}
+      className="mb-2 sm:mb-0 underline text-[15px] cursor-pointer"
+    >
+      CSV template
+    </div>
+    <Button
+      type="button"
+      onClick={handleUpload}
+      className="w-full sm:w-auto text-[15px] px-4 py-2 h-10"
+      disabled={isImporting}
+    >
+      {isImporting ? 'Parsing...' : 'Choose a file'}
+    </Button>
+  </>
+);
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 0000000000000..f63302b3473cb
--- /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<FileUploadAreaRef>(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 (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent
+        className={
+          isPreviewMode ? 'sm:max-w-[720px] flex-col' : 'sm:max-w-[480px]'
+        }
+      >
+        <DialogHeader>
+          <DialogTitle>
+            {isFormatError
+              ? 'Incorrect import format'
+              : isPreviewMode
+                ? isImported
+                  ? 'Import results'
+                  : 'Confirm import'
+                : 'Import'}
+          </DialogTitle>
+        </DialogHeader>
+        <div className="text-[15px] mt-3">
+          {isFormatError ? (
+            <ImportErrorContent />
+          ) : isPreviewMode ? (
+            <ImportPreviewContent
+              parsedUsers={parsedUsers}
+              isImported={isImported}
+            />
+          ) : (
+            <ImportInitialContent
+              passwordLimits={passwordLimits}
+              fileUploadRef={fileUploadRef}
+              onFileSelected={handleFileSelected}
+            />
+          )}
+        </div>
+
+        <ImportUsersFooter
+          isFormatError={isFormatError}
+          isPreviewMode={isPreviewMode}
+          isImporting={isImporting}
+          isImported={isImported}
+          resetFormatError={resetFormatError}
+          cancelImport={cancelImport}
+          handleConfirm={handleConfirm}
+          handleUpload={handleUpload}
+          parsedUsers={parsedUsers}
+        />
+      </DialogContent>
+    </Dialog>
+  );
+}
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 0000000000000..1a224dbb9cd12
--- /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<ParsedUser[]>([]);
+  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<typeof user, { __typename: 'UserType' }> =>
+          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 fddf2e00fc55f..15726d85286cb 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 f6d7f36995d4e..5b0aa079e1438 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<UserInput>) => 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<Partial<UserInput>>(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 && (
+            <>
+              <Separator />
+              <InputItem
+                label="Password"
+                field="password"
+                value={changes.password}
+                onChange={setField}
+                optional
+                placeholder="Enter password"
+              />
+            </>
+          )}
         </div>
 
         <div className="border rounded-md">
@@ -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 (
-    <div className="p-3">
-      <Label className="text-[15px] leading-6 font-medium mb-1.5">
+    <div className="flex flex-col gap-1.5 p-3">
+      <Label
+        className="text-[15px] font-medium flex-wrap flex"
+        style={{ lineHeight: '1.6rem' }}
+      >
         {label}
+        {optional && (
+          <span
+            className="font-normal ml-1"
+            style={{ color: cssVarV2('text/secondary') }}
+          >
+            (optional)
+          </span>
+        )}
       </Label>
       <Input
         type="text"
@@ -210,6 +243,8 @@ const validateUpdateUser = (user: Partial<UserInput>) => {
 
 export function CreateUserForm({ onComplete }: { onComplete: () => void }) {
   const { create, creating } = useCreateUser();
+  const serverConfig = useServerConfig();
+  const passwordLimits = serverConfig.credentialsRequirement.password;
   useEffect(() => {
     if (creating) {
       return () => {
@@ -219,12 +254,30 @@ export function CreateUserForm({ onComplete }: { onComplete: () => void }) {
 
     return;
   }, [creating, onComplete]);
+
+  const handleCreateUser = useCallback(
+    (user: UserInput) => {
+      const emailValidation = validateEmails([user]);
+      const passwordValidation = validatePassword(
+        user.password,
+        passwordLimits
+      );
+      if (!passwordValidation.valid || !emailValidation[0].valid) {
+        toast.error(passwordValidation.error || emailValidation[0].error);
+        return;
+      }
+      create(user);
+    },
+    [create, passwordLimits]
+  );
+
   return (
     <UserForm
       title="Create User"
       onClose={onComplete}
-      onConfirm={create}
+      onConfirm={handleCreateUser}
       onValidate={validateCreateUser}
+      showOption={true}
     />
   );
 }
diff --git a/packages/frontend/admin/src/modules/accounts/components/user-table.tsx b/packages/frontend/admin/src/modules/accounts/components/user-table.tsx
index a3fa2f09aa726..fde484c5ad9d6 100644
--- a/packages/frontend/admin/src/modules/accounts/components/user-table.tsx
+++ b/packages/frontend/admin/src/modules/accounts/components/user-table.tsx
@@ -11,7 +11,7 @@ export const UserTable: React.FC<UserTableProps> = ({ users }) => {
   return (
     <div className="max-h-[300px] overflow-y-auto border rounded-md">
       <table className="w-full border-collapse">
-        <thead className="bg-gray-50 sticky top-0">
+        <thead className="bg-white sticky top-0">
           <tr>
             <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
               Username
@@ -19,6 +19,9 @@ export const UserTable: React.FC<UserTableProps> = ({ users }) => {
             <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
               Email
             </th>
+            <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+              Password
+            </th>
             <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
               Status
             </th>
@@ -37,10 +40,26 @@ export const UserTable: React.FC<UserTableProps> = ({ users }) => {
                 {user.name || '-'}
               </td>
               <td
-                className={`py-2 px-4 text-sm truncate max-w-[200px] ${user.valid === false ? 'text-red-500' : 'text-gray-900'}`}
+                className={`py-2 px-4 text-sm truncate max-w-[200px] ${
+                  user.valid === false &&
+                  (user.error?.toLowerCase().includes('email') ||
+                    !user.error?.toLowerCase().includes('password'))
+                    ? 'text-red-500'
+                    : 'text-gray-900'
+                }`}
               >
                 {user.email}
               </td>
+              <td
+                className={`py-2 px-4 text-sm truncate max-w-[150px] ${
+                  user.valid === false &&
+                  user.error?.toLowerCase().includes('password')
+                    ? 'text-red-500'
+                    : 'text-gray-900'
+                }`}
+              >
+                {user.password || '-'}
+              </td>
               <td className="py-2 px-4 text-sm">
                 {user.importStatus === ImportStatus.Success ? (
                   <span className="text-gray-900">
diff --git a/packages/frontend/admin/src/modules/accounts/schema.ts b/packages/frontend/admin/src/modules/accounts/schema.ts
index e8711a4cf8dd5..d609c3a9424e9 100644
--- a/packages/frontend/admin/src/modules/accounts/schema.ts
+++ b/packages/frontend/admin/src/modules/accounts/schema.ts
@@ -4,5 +4,6 @@ export type UserType = ListUsersQuery['users'][0];
 export type UserInput = {
   name: string;
   email: string;
+  password?: string;
   features: FeatureType[];
 };
diff --git a/packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts b/packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts
index 949f379112444..dc1b2b699340f 100644
--- a/packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts
+++ b/packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts
@@ -5,6 +5,7 @@ import { emailRegex } from '../../../utils';
 export interface ParsedUser {
   name: string | null;
   email: string;
+  password?: string;
   valid?: boolean;
   error?: string;
   importStatus?: ImportStatus;
@@ -17,6 +18,41 @@ export enum ImportStatus {
   Processing = 'processing',
 }
 
+export const validatePassword = (
+  password: string | undefined,
+  passwordLimits: { minLength: number; maxLength: number }
+): { valid: boolean; error?: string } => {
+  // if password is empty, it is valid
+  if (!password || password.trim() === '') {
+    return { valid: true };
+  }
+
+  // check password length
+  if (
+    password.length < passwordLimits.minLength ||
+    password.length > passwordLimits.maxLength
+  ) {
+    return {
+      valid: false,
+      error: 'Invalid password format',
+    };
+  }
+
+  // TODO(@Jimmfly): check password contains at least one letter and one number
+
+  // const hasLetter = /[a-zA-Z]/.test(password);
+  // const hasNumber = /[0-9]/.test(password);
+
+  // if (!hasLetter || !hasNumber) {
+  //   return {
+  //     valid: false,
+  //     error: 'Invalid password format',
+  //   };
+  // }
+
+  return { valid: true };
+};
+
 /**
  * Validates email addresses for duplicates and format
  */
@@ -53,6 +89,7 @@ export const getValidUsersToImport = (users: ParsedUser[]) => {
     .map(user => ({
       name: user.name || undefined,
       email: user.email,
+      password: user.password,
     }));
 };
 
@@ -60,7 +97,7 @@ export const getValidUsersToImport = (users: ParsedUser[]) => {
  * Downloads a CSV template for user import
  */
 export const downloadCsvTemplate = () => {
-  const csvContent = 'Username,Email\n,example@example.com';
+  const csvContent = 'Username,Email,Password\n,example@example.com,';
   downloadCsv(csvContent, 'user_import_template.csv');
 };
 
@@ -69,10 +106,10 @@ export const downloadCsvTemplate = () => {
  */
 export const exportImportResults = (results: ParsedUser[]) => {
   const csvContent = [
-    'Username,Email,status',
+    'Username,Email,Password,Status',
     ...results.map(
       user =>
-        `${user.name || ''},${user.email},${user.importStatus}${user.importError ? ` (${user.importError})` : ''}`
+        `${user.name || ''},${user.email},${user.password || ''},${user.importStatus}${user.importError ? ` (${user.importError})` : ''}`
     ),
   ].join('\n');
 
@@ -138,6 +175,7 @@ export const processCSVFile = async (
     const users = dataRows.map(row => ({
       name: row[0]?.trim() || null,
       email: row[1]?.trim() || '',
+      password: row[2]?.trim() || undefined,
     }));
 
     const usersWithEmail = users.filter(user => user.email);
@@ -164,3 +202,34 @@ export const processCSVFile = async (
     onError();
   }
 };
+
+/**
+ * Validate users
+ */
+export const validateUsers = (
+  users: ParsedUser[],
+  passwordLimits: { minLength: number; maxLength: number }
+): ParsedUser[] => {
+  // validate emails
+  const emailValidatedUsers = validateEmails(users);
+
+  // validate password
+  return emailValidatedUsers.map(user => {
+    // if email is invalid, return
+    if (user.valid === false) {
+      return user;
+    }
+
+    // validate password
+    const passwordValidation = validatePassword(user.password, passwordLimits);
+    if (!passwordValidation.valid) {
+      return {
+        ...user,
+        valid: false,
+        error: passwordValidation.error,
+      };
+    }
+
+    return user;
+  });
+};


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx (1)</summary><blockquote>

`1-52`: **Well-structured CSV format guidance component**

This new component effectively communicates CSV import requirements to users, including dynamic password length constraints. The styling using theme variables ensures consistency with the application's design system.

I particularly like the bullet-point formatting with visual indicators, making the instructions clear and scannable.


One optional enhancement to consider:

```diff
-<p>CSV file includes username, email, and password.</p>
+<p>CSV file should include the following columns:</p>
```

This minor wording change could make the purpose of the list clearer, but the current implementation is already very effective.

</blockquote></details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx (1)</summary><blockquote>

`64-65`: **Nit: unify flex direction for both dialog widths**

Only the “preview” branch adds the `flex-col` class.  
If vertical layout is required in both cases, consider always applying it to keep markup predictable.

```diff
-  isPreviewMode ? 'sm:max-w-[720px] flex-col' : 'sm:max-w-[480px]'
+  `${isPreviewMode ? 'sm:max-w-[720px]' : 'sm:max-w-[480px]'} flex-col`
```

</blockquote></details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx (1)</summary><blockquote>

`69-73`: **Wrap error message in semantic markup for accessibility**

Returning a bare string works, but screen-readers can mis-interpret plain text that is not wrapped in a landmark.  
Enclosing the message in a `<p>` (or `<div role="alert">`) improves semantics and ARIA support.

```diff
-export const ImportErrorContent: FC<ImportErrorContentProps> = ({
-  message = 'You need to import the accounts by importing a CSV file in the correct format. Please download the CSV template.',
-}) => {
-  return message;
-};
+export const ImportErrorContent: FC<ImportErrorContentProps> = ({
+  message = 'You need to import the accounts by importing a CSV file in the correct format. Please download the CSV template.',
+}) => <p role="alert">{message}</p>;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 41d404f7f8328312d2c193181642a670f72e5682 and 7391d24e863e0d77e93ae9ee36313f488d76c2f7.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx` (2 hunks)
* `packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx` (0 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/use-user-management.ts` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/user-form.tsx` (9 hunks)
* `packages/frontend/admin/src/modules/accounts/components/user-table.tsx` (2 hunks)
* `packages/frontend/admin/src/modules/accounts/schema.ts` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts` (6 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (5)</summary>

<details>
<summary>packages/frontend/admin/src/modules/accounts/components/use-user-management.ts (1)</summary><blockquote>

<details>
<summary>packages/frontend/admin/src/modules/accounts/schema.ts (1)</summary>

* `UserInput` (4-9)

</details>

</blockquote></details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx (2)</summary><blockquote>

<details>
<summary>packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts (1)</summary>

* `downloadCsvTemplate` (99-102)

</details>
<details>
<summary>packages/backend/server/src/core/user/resolver.ts (1)</summary>

* `user` (56-77)

</details>

</blockquote></details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx (4)</summary><blockquote>

<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/file-upload-area.tsx (1)</summary>

* `FileUploadAreaRef` (18-20)

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts (1)</summary>

* `useImportUsersState` (26-193)

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx (3)</summary>

* `ImportErrorContent` (69-73)
* `ImportPreviewContent` (16-31)
* `ImportInitialContent` (45-60)

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx (1)</summary>

* `ImportUsersFooter` (24-63)

</details>

</blockquote></details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/user-form.tsx (2)</summary><blockquote>

<details>
<summary>packages/frontend/admin/src/modules/accounts/schema.ts (1)</summary>

* `UserInput` (4-9)

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts (2)</summary>

* `validateEmails` (59-81)
* `validatePassword` (21-54)

</details>

</blockquote></details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts (1)</summary><blockquote>

<details>
<summary>packages/backend/server/src/core/user/resolver.ts (1)</summary>

* `user` (56-77)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (31)</summary><blockquote>

<details>
<summary>packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx (1)</summary>

`20-20`: **LGTM: Import path updated correctly**

This import path change reflects the restructuring of the import users dialog to a more modular approach, aligning with the broader updates to support password handling in the user management workflow.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/schema.ts (1)</summary>

`7-7`: **LGTM: Good schema extension for password support**

Adding an optional password field to the UserInput type is appropriate for supporting password handling during user creation while maintaining backward compatibility.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/use-user-management.ts (2)</summary>

`50-50`: **LGTM: Function signature updated correctly**

Function signature has been properly updated to include the optional password parameter, consistent with the UserInput type definition.

---

`56-56`: **Good handling of empty password values**

The conditional logic properly distinguishes between "no password provided" (undefined) and "an empty password provided" (empty string), ensuring empty strings aren't sent to the backend.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/user-table.tsx (2)</summary>

`14-14`: **LGTM: Improved header styling**

Changing the background to white improves visibility and maintains consistency with other components.

---

`43-49`: **LGTM: Improved error handling logic**

The refined conditional logic for styling email errors is more precise, correctly distinguishing between email-specific and password-specific validation issues.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx (5)</summary>

`174-179`: **Event handler correction for Edit action**

The code now correctly attaches the `handleEdit` function to the Edit menu item, ensuring proper functionality when users select this option.

---

`180-186`: **Enhanced password management UI**

The dropdown menu now dynamically displays either "Reset Password" or "Setup Account" based on whether the user already has a password, making the UI more contextually accurate. This change aligns with the new password management features introduced in this PR.

---

`192-194`: **Improved formatting for Enable Email option**

The menu item's formatting has been updated for better readability by moving the icon and text to separate lines.

---

`202-204`: **Improved formatting for Disable & Delete option**

The menu item's formatting has been updated for better readability by moving the icon and text to separate lines.

---

`210-212`: **Improved formatting for Delete option**

The menu item's formatting has been updated for better readability by moving the icon and text to separate lines.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/user-form.tsx (8)</summary>

`26-27`: **Added password field display option**

The form now supports displaying an optional password field, controlled by the `showOption` prop. This modification allows the same form component to be used in different contexts with or without password input.



Also applies to: 36-37

---

`40-48`: **Password field initialization in default user state**

The default user state now properly includes a password field initialized from `defaultValue`. The dependency array also correctly includes `defaultValue` to ensure the form resets properly when the default values change.

---

`70-78`: **Form state reset after submission**

The form now correctly resets its state to the default user after a successful submission, providing a better user experience for repeated form submissions.

---

`126-138`: **Conditional password input field**

The password input field is now conditionally rendered based on the `showOption` prop, with proper separation using a separator line. This implementation ensures the password field appears only when needed.

---

`190-198`: **Added optional flag for form inputs**

The `InputItem` component now supports an `optional` prop, allowing form fields to be marked as optional in the UI.

---

`209-224`: **Enhanced input label with optional indicator**

The input label now displays an "(optional)" indicator when applicable, using appropriate styling for secondary text. This provides better visual cues to users about field requirements.

---

`246-272`: **Added comprehensive validation for user creation**

The new `handleCreateUser` function adds proper validation for both email and password fields before user creation. It uses the `validateEmails` and `validatePassword` utilities and displays appropriate error messages using toast notifications.

This validation ensures that users cannot be created with invalid credentials, improving data integrity and user experience.

---

`280-281`: **Enabled password input in create user form**

The `CreateUserForm` now shows the password field by setting `showOption` to true, allowing administrators to set initial passwords when creating users.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx (5)</summary>

`7-19`: **Well-structured props interface**

The component's props interface is comprehensive and clearly defines all the necessary information and callbacks for managing the import workflow states. This provides good documentation for component usage and ensures type safety.

---

`24-63`: **Adaptive footer component with dynamic styling**

The main `ImportUsersFooter` component intelligently renders different footer contents based on the current import state. The dynamic class application for layout adjustment based on preview mode is particularly effective for maintaining appropriate spacing and alignment across different states.

---

`73-93`: **Error state footer with helpful template link**

The `FormatErrorFooter` component provides users with both a way to download a template CSV file and a clear action to dismiss the error, helping users recover from formatting issues.

---

`108-136`: **Contextual button labels and disabled states**

The `PreviewModeFooter` component displays appropriate button labels based on the import status ("Importing...", "Export", or "Confirm Import") and correctly disables buttons during processing. The disabled state logic prevents users from confirming import while users are still being processed, avoiding potential data inconsistency.

---

`147-168`: **Initial state footer with template download**

The `InitialFooter` component provides a convenient way for users to download a CSV template before uploading, and clearly indicates when parsing is in progress. This improves user guidance through the import process.

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx (1)</summary>

`32-35`: **Guard against missing server config to avoid runtime crash**

`useServerConfig()` may briefly return `undefined` (e.g. during first render while requesting the config).  
Accessing `serverConfig.credentialsRequirement.password` without a null-check will raise an exception and blank the whole dialog.

```diff
-const passwordLimits = serverConfig.credentialsRequirement.password;
+const passwordLimits = serverConfig?.credentialsRequirement?.password;
+
+if (!passwordLimits) {
+  // Fallback to a sensible default or early-return a skeleton/loader UI
+  return null;
+}
```
[tagged: flag_critical_issue]

</details>
<details>
<summary>packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts (6)</summary>

`8-8`: **Interface update looks good for optional password support.**

Adding an optional password field to the ParsedUser interface aligns with the goal of supporting user creation with passwords.

---

`92-92`: **LGTM: Password inclusion in valid users extraction.**

Correctly including the optional password field when extracting valid users for import.

---

`100-100`: **LGTM: CSV template updated for password column.**

The CSV template correctly includes the Password column in the header.

---

`109-109`: **LGTM: Export results header updated for password column.**

The export header correctly includes the Password column.

---

`112-112`: **LGTM: Export row mapping updated for password field.**

The user data to CSV row mapping correctly includes the password field.

---

`178-178`: **LGTM: CSV processing updated to parse password field.**

The CSV processing logic correctly parses the password from the third column of each row.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-04T17:50:56Z
user: login: forehalo
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-06T06:49:56Z

Review comments:
diff_hunk: @@ -37,10 +40,26 @@ export const UserTable: React.FC<UserTableProps> = ({ users }) => {
                 {user.name || '-'}
               </td>
               <td
-                className={`py-2 px-4 text-sm truncate max-w-[200px] ${user.valid === false ? 'text-red-500' : 'text-gray-900'}`}
+                className={`py-2 px-4 text-sm truncate max-w-[200px] ${
+                  user.valid === false &&
+                  (user.error?.toLowerCase().includes('email') ||
+                    !user.error?.toLowerCase().includes('password'))
+                    ? 'text-red-500'
+                    : 'text-gray-900'
+                }`}
               >
                 {user.email}
               </td>
+              <td
+                className={`py-2 px-4 text-sm truncate max-w-[150px] ${
+                  user.valid === false &&
+                  user.error?.toLowerCase().includes('password')
+                    ? 'text-red-500'
+                    : 'text-gray-900'
+                }`}
+              >
+                {user.password || '-'}
+              </td>
path: packages/frontend/admin/src/modules/accounts/components/user-table.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Security concern: Plain text password display**

Similar to the header issue, displaying passwords in plain text is a security risk. Even administrators shouldn't see user passwords.


Replace with a more secure approach:

```diff
-{user.password || '-'}
+{user.password ? 'Password set' : 'No password'}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
              <td
                className={`py-2 px-4 text-sm truncate max-w-[150px] ${
                  user.valid === false &&
                  user.error?.toLowerCase().includes('password')
                    ? 'text-red-500'
                    : 'text-gray-900'
                }`}
              >
-                {user.password || '-'}
+                {user.password ? 'Password set' : 'No password'}
              </td>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T17:50:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,14 +11,17 @@
   return (
     <div className="max-h-[300px] overflow-y-auto border rounded-md">
       <table className="w-full border-collapse">
-        <thead className="bg-gray-50 sticky top-0">
+        <thead className="bg-white sticky top-0">
           <tr>
             <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
               Username
             </th>
             <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
               Email
             </th>
+            <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+              Password
+            </th>
path: packages/frontend/admin/src/modules/accounts/components/user-table.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Security concern: Displaying passwords in clear text**

Adding a password column to display passwords in plain text raises security concerns, even in an admin interface. Consider masking passwords or showing password strength indicators instead.



Consider implementing a more secure approach:

```diff
-<th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
-  Password
-</th>
+<th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+  Password Status
+</th>
```

And then in the data cell:

```diff
-{user.password || '-'}
+{user.password ? 'Set' : 'Not set'}
```

---

🌐 Web query:

```web
Is displaying passwords in plain text in admin interfaces a security best practice?
```

💡 Result:

Displaying passwords in plain text in admin interfaces is absolutely not a security best practice. It represents a significant security risk that can lead to various vulnerabilities and breaches.

## Why Plain Text Password Display Is Dangerous

**Increased Risk of Unauthorized Access**
Displaying passwords in plain text makes them easy targets for hackers and malicious actors. If an admin interface is compromised, attackers can immediately access all visible passwords without additional effort[4]. Even a momentary breach or shoulder surfing can expose critical credentials.

**Internal Threats**
Employees with malicious intent or those who are simply negligent can easily access and misuse information when passwords are displayed in plain text[4]. This creates opportunities for internal data breaches that can damage an organization's reputation and compromise sensitive information.

**Regulatory Compliance Issues**
Many industries have regulations that mandate the protection of sensitive data. Displaying passwords in plain text can lead to non-compliance with laws like GDPR and HIPAA, potentially resulting in significant fines and legal complications[4].

## Password Security Best Practices

**Mask Passwords**
Password management systems should ensure that passwords are not displayed on the screen when being entered[6]. This is a fundamental security practice to prevent visual exposure of credentials.

**Proper Password Storage**
Passwords should:
- Be stored and transmitted in protected (encrypted or hashed) form[6]
- Be kept separately from application system data[6]
- Never be stored in plaintext as this can result in system compromise[1]

**Password Management Guidelines**
Organizations should implement password policies that:
- Maintain a minimum length requirement (fourteen characters recommended)[8]
- Ban common passwords[8]
- Enforce multi-factor authentication[8]
- Avoid mandatory periodic password resets for user accounts[8]

**System-Level Account Security**
For admin and system-level accounts:
- Use more lengthy and complex passwords[10]
- Change passwords whenever someone with knowledge of the password changes job responsibilities or terminates employment[10]
- Never use the same password for multiple administrator accounts[10]

## Consequences of Plain Text Password Display

**Security Breaches**
Plain text storage and display of passwords has led to massive security incidents. For example, in 2024, the RockYou2024 breach exposed over 10 billion unique credentials that were stored in plaintext[2].

**User Impact**
When passwords are compromised due to plain text display or storage, users are put at significant risk, especially since many people reuse passwords across multiple sites[3]. This can lead to secondary crimes as attackers try to access other systems using the same credentials.

**Business Consequences**
Organizations that display passwords in plain text face:
- Costly incident response requirements[4]
- Erosion of customer trust and loyalty[4]
- Significant operational disruptions[4]
- Financial losses from breaches and compliance violations[4]

Instead of displaying passwords in plain text, organizations should implement secure password management systems with proper encryption, masking of sensitive data, and robust authentication protocols.

Citations:

- 1: https://owasp.org/www-community/vulnerabilities/Password_Plaintext_Storage
- 2: https://nordpass.com/blog/plaintext-passwords/
- 3: https://www.securecodewarrior.com/article/coders-conquer-security-infrastructure-as-codesensitive-data-storage-plaintext-storage-of-passwords
- 4: https://www.metomic.io/resource-centre/plain-text-credentials
- 5: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
- 6: https://www.securden.com/blog/top-10-password-policies.html
- 7: https://palant.info/2018/10/25/should-your-next-web-based-login-form-avoid-sending-passwords-in-clear-text/
- 8: https://learn.microsoft.com/en-us/microsoft-365/admin/misc/password-policy-recommendations?view=o365-worldwide
- 9: https://www.iansresearch.com/resources/all-blogs/post/security-blog/2021/05/03/password-policy-best-practices-for-your-business
- 10: https://www.cmu.edu/iso/governance/guidelines/password-management.html

---



</details>

**Prevent plain-text password exposure in the admin UI**

Displaying user passwords in clear text poses a critical security risk—any compromise of the admin interface (even brief or internal “shoulder-surfing”) immediately exposes all credentials and may violate compliance requirements (e.g. GDPR, HIPAA). Mask or omit the actual password and instead surface non-sensitive status.

Please update the table column in:  
- packages/frontend/admin/src/modules/accounts/components/user-table.tsx (around lines 22–24)

Suggested change:

```diff
- <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
-   Password
- </th>
+ <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
+   Password Status
+ </th>
```

And in the corresponding data cell:

```diff
- {user.password || '-'}
+ {user.password ? 'Set' : 'Not set'}
```

Consider further enhancements (e.g., password strength, last reset date) rather than exposing actual passwords. 

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            <th className="py-2 px-4 border-b text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Password Status
            </th>
...
            <td className="py-2 px-4 border-b">
              {user.password ? 'Set' : 'Not set'}
            </td>
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T17:50:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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<ParsedUser[]>([]);
+  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<typeof user, { __typename: 'UserType' }> =>
+          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]);
path: packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Skip API call when there are no valid users to import**

When every row is invalid, `validUsersToImport` is an empty array and you still hit the network.  
Returning early avoids an unnecessary request and an inevitable error toast from the server.

```diff
   const confirmImport = useAsyncCallback(async () => {
     setIsImporting(true);
     try {
       const validUsersToImport = getValidUsersToImport(parsedUsers);
+
+      if (validUsersToImport.length === 0) {
+        toast.info('No valid users to import');
+        setIsImporting(false);
+        return;
+      }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const confirmImport = useAsyncCallback(async () => {
    setIsImporting(true);
    try {
      const validUsersToImport = getValidUsersToImport(parsedUsers);

      if (validUsersToImport.length === 0) {
        toast.info('No valid users to import');
        setIsImporting(false);
        return;
      }

      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]);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T17:50:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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<ParsedUser[]>([]);
+  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<typeof user, { __typename: 'UserType' }> =>
+          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) {
path: packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Stale closure risk – depend on the full `parsedUsers` array, not just its length**

`importUsersCallback` uses the actual contents of `parsedUsers` (e.g. `failedUserErrorMap`), but the dependency list only contains `parsedUsers.length`.  
If the user list changes while preserving the same length (e.g. re-importing a different file with the same number of rows), the callback will retain a stale copy and mark the wrong rows as succeeded/failed.

```diff
-  },
-  [parsedUsers.length]
+  },
+  [parsedUsers]               // re-create whenever the list identity changes
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const importUsersCallback = useCallback(
    (result: UserImportReturnType) => {
      const successfulUsers = result.filter(
        (user): user is Extract<typeof user, { __typename: 'UserType' }> =>
          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) {
        // handle failures…
      }
    },
    [parsedUsers] // re-create whenever the list identity changes
  );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T17:50:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +18,41 @@ export enum ImportStatus {
   Processing = 'processing',
 }
 
+export const validatePassword = (
+  password: string | undefined,
+  passwordLimits: { minLength: number; maxLength: number }
+): { valid: boolean; error?: string } => {
+  // if password is empty, it is valid
+  if (!password || password.trim() === '') {
+    return { valid: true };
+  }
+
+  // check password length
+  if (
+    password.length < passwordLimits.minLength ||
+    password.length > passwordLimits.maxLength
+  ) {
+    return {
+      valid: false,
+      error: 'Invalid password format',
+    };
+  }
+
+  // TODO(@Jimmfly): check password contains at least one letter and one number
+
+  // const hasLetter = /[a-zA-Z]/.test(password);
+  // const hasNumber = /[0-9]/.test(password);
+
+  // if (!hasLetter || !hasNumber) {
+  //   return {
+  //     valid: false,
+  //     error: 'Invalid password format',
+  //   };
+  // }
+
+  return { valid: true };
+};
path: packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve password validation with more specific error messages.**

The password validation function works correctly, but the error message "Invalid password format" doesn't inform users about the specific requirements (min/max length).

Also, there's a TODO comment about checking for letters and numbers with commented-out implementation. Consider either implementing this requirement now or documenting when this will be addressed.


```diff
-      error: 'Invalid password format',
+      error: `Password must be between ${passwordLimits.minLength} and ${passwordLimits.maxLength} characters`,
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export const validatePassword = (
  password: string | undefined,
  passwordLimits: { minLength: number; maxLength: number }
): { valid: boolean; error?: string } => {
  // if password is empty, it is valid
  if (!password || password.trim() === '') {
    return { valid: true };
  }

  // check password length
  if (
    password.length < passwordLimits.minLength ||
    password.length > passwordLimits.maxLength
  ) {
    return {
      valid: false,
      error: `Password must be between ${passwordLimits.minLength} and ${passwordLimits.maxLength} characters`,
    };
  }

  // TODO(@Jimmfly): check password contains at least one letter and one number

  // const hasLetter = /[a-zA-Z]/.test(password);
  // const hasNumber = /[0-9]/.test(password);

  // if (!hasLetter || !hasNumber) {
  //   return {
  //     valid: false,
  //     error: 'Invalid password format',
  //   };
  // }

  return { valid: true };
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T17:50:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,3 +202,34 @@
     onError();
   }
 };
+
+/**
+ * Validate users
+ */
+export const validateUsers = (
+  users: ParsedUser[],
+  passwordLimits: { minLength: number; maxLength: number }
+): ParsedUser[] => {
+  // validate emails
+  const emailValidatedUsers = validateEmails(users);
+
+  // validate password
+  return emailValidatedUsers.map(user => {
+    // if email is invalid, return
+    if (user.valid === false) {
+      return user;
+    }
+
+    // validate password
+    const passwordValidation = validatePassword(user.password, passwordLimits);
+    if (!passwordValidation.valid) {
+      return {
+        ...user,
+        valid: false,
+        error: passwordValidation.error,
+      };
+    }
+
+    return user;
+  });
+};
path: packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**User validation logic is complete but could benefit from more descriptive errors.**

The new validateUsers function efficiently validates both emails and passwords, correctly skipping password validation if email is already invalid. However, consider enhancing error messages to provide more actionable feedback to users.

For example, in the password validation response, you could include specific requirements that failed (like length requirements).


Also, verify this function is called in the appropriate places in the import workflow:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check where validateUsers is called in the codebase
rg "validateUsers" --type ts -A 2 -B 2
```

Length of output: 1748

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the definition of validatePassword
rg -n "export function validatePassword" -A 5 --type ts
```

Length of output: 57

---

🏁 Script executed:

```shell
#!/bin/bash
# List utility files to locate the validatePassword source
ls -1 packages/frontend/admin/src/modules/accounts/utils

# Search for validatePassword definition or usage in the utils directory
rg -n "validatePassword" -C 5 packages/frontend/admin/src/modules/accounts/utils
```

Length of output: 2263

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the implementation of validatePassword including error messages
rg -n "export const validatePassword" -A 50 packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts
```

Length of output: 1651

---



</details>

**Enhance password validation error messages**

The `validatePassword` helper currently returns a generic `'Invalid password format'` when the length check fails (and will do the same for the pending letter/number checks). To give users more actionable feedback, update these error messages to include specific requirements. For example:

• `Password must be between {minLength} and {maxLength} characters.`  
• `Password must contain at least one letter and one number.`  

Call site for `validateUsers` (no other usages found):  
- packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-04T17:50:56Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-02T09:51:37Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

"""
## Walkthrough

This update introduces comprehensive support for importing users with optional passwords via CSV in the admin frontend. It restructures the user import dialog into modular components, adds password validation and error handling, and extends the user creation flow to accept passwords. The CSV utilities, user table, and schema are updated to handle password fields, with validation based on server-configured constraints. The user form and creation logic are enhanced to include password input and validation. The import dialog is refactored for improved state management and user feedback, and the UI is updated to reflect password-related information throughout the import and creation processes.

## Changes

| File(s)                                                                                      | Change Summary |
|----------------------------------------------------------------------------------------------|---------------|
| `.../data-table-row-actions.tsx`                                                             | Reordered dropdown menu items, swapped event handlers for "Edit" and "Reset Password", updated icons and labels, and adjusted text formatting for menu items. No exported entity changes. |
| `.../data-table-toolbar.tsx`                                                                 | Updated import path for `ImportUsersDialog` from `'./import-users-dialog'` to `'./import-users'`. No logic changes. |
| `.../import-users-dialog.tsx`                                                                | Deleted file. Removed the original `ImportUsersDialog` component and its complete CSV import workflow implementation. |
| `.../import-users/csv-format-guidance.tsx`                                                   | Added new `CsvFormatGuidance` component to display CSV format guidelines, with dynamic password constraints and styled list. |
| `.../import-users/import-content.tsx`                                                        | Introduced `ImportPreviewContent`, `ImportInitialContent`, and `ImportErrorContent` components for modular import dialog content rendering. |
| `.../import-users/import-footer.tsx`                                                         | Added `ImportUsersFooter` component with subcomponents for dialog footer actions, conditionally rendered based on import state. |
| `.../import-users/index.tsx`                                                                 | Added new `ImportUsersDialog` component using modular content, state management hook, and dynamic UI transitions for user import workflow. |
| `.../import-users/use-import-users-state.ts`                                                 | Introduced `useImportUsersState` hook to manage user import state, file handling, validation, import execution, error handling, and result export. |
| `.../use-user-management.ts`                                                                 | Updated `useCreateUser` hook and its `create` function to accept and conditionally include a `password` field for user creation. |
| `.../user-form.tsx`                                                                         | Enhanced `UserForm` and `CreateUserForm` to support optional password input, validation, and improved form reset and optional field labeling. |
| `.../user-table.tsx`                                                                        | Changed table header to white, added "Password" column, displayed password values, and refined error-based cell styling for email and password fields. |
| `.../schema.ts`                                                                             | Added optional `password?: string` property to the exported `UserInput` type. |
| `.../utils/csv-utils.ts`                                                                    | Extended `ParsedUser` interface with `password`, added password validation, updated CSV template and import/export logic to handle passwords, and introduced `validateUsers` for combined email/password validation. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Admin as Admin User
    participant Dialog as ImportUsersDialog
    participant State as useImportUsersState
    participant Utils as csv-utils
    participant Server as Backend API

    Admin->>Dialog: Open Import Users Dialog
    Dialog->>State: Initialize import state
    Admin->>Dialog: Upload CSV file
    Dialog->>State: onFileSelected(file)
    State->>Utils: processCSVFile(file)
    Utils-->>State: Parsed users (with email, password, etc.)
    State->>Dialog: Set preview mode, show parsed users
    Admin->>Dialog: Confirm Import
    Dialog->>State: onConfirmImport()
    State->>Server: Import valid users (with optional passwords)
    Server-->>State: Import results (success/failure per user)
    State->>Dialog: Show import results, enable export if needed
    Admin->>Dialog: Export failed results / Close dialog
    Dialog->>State: Reset state, close dialog
```

## Assessment against linked issues

| Objective                                                                                      | Addressed | Explanation                          |
|------------------------------------------------------------------------------------------------|-----------|------------------------------------|
| Support importing users with optional passwords in the admin panel (AF-2494)                   | ✅        |                                    |

## Possibly related PRs

- [toeverything/AFFiNE#11978](https://github.com/toeverything/AFFiNE/pull/11978): Adds backend support for user creation with optional password, complementing this PR's frontend CSV import and password handling.

## Suggested labels

`app:server`

## Poem

> 🐇  
> A CSV arrives, with secrets to keep,  
> Passwords and emails in columns so neat.  
> The dialog guides with a gentle hop,  
> Validates fields so errors will stop.  
> Now users can join with a pass or a dash—  
> Importing with ease, in a single flash!  
> Hooray for the code, and the rabbits who cheer,  
> For password support is finally here!  
>
"""

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7391d24e863e0d77e93ae9ee36313f488d76c2f7 and e1cbdfab4d2e3ea92f929f01978645a014862ad7.

</details>

<details>
<summary>📒 Files selected for processing (13)</summary>

* `packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx` (2 hunks)
* `packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx` (0 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/use-user-management.ts` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/components/user-form.tsx` (9 hunks)
* `packages/frontend/admin/src/modules/accounts/components/user-table.tsx` (2 hunks)
* `packages/frontend/admin/src/modules/accounts/schema.ts` (1 hunks)
* `packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts` (6 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* packages/frontend/admin/src/modules/accounts/components/import-users-dialog.tsx

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* packages/frontend/admin/src/modules/accounts/components/data-table-toolbar.tsx
* packages/frontend/admin/src/modules/accounts/components/import-users/import-content.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (10)</summary>

* packages/frontend/admin/src/modules/accounts/schema.ts
* packages/frontend/admin/src/modules/accounts/components/import-users/use-import-users-state.ts
* packages/frontend/admin/src/modules/accounts/components/user-table.tsx
* packages/frontend/admin/src/modules/accounts/components/data-table-row-actions.tsx
* packages/frontend/admin/src/modules/accounts/components/use-user-management.ts
* packages/frontend/admin/src/modules/accounts/components/import-users/csv-format-guidance.tsx
* packages/frontend/admin/src/modules/accounts/components/import-users/index.tsx
* packages/frontend/admin/src/modules/accounts/components/import-users/import-footer.tsx
* packages/frontend/admin/src/modules/accounts/components/user-form.tsx
* packages/frontend/admin/src/modules/accounts/utils/csv-utils.ts

</details>

</details>
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAMxJqAAo0WmZ4DABKLgZ/ag9sREpIAHd1WB40RERi/ApaaJLK+2wBNNwaejkw2HzCvgApeFYAMS95THpUeCUMcVj4KKbbSAxHASKARgAmDe2NGB7rO1RYfEQO/ixcQ/9uM/Va8dDrjwBVGwAZLlh27kQOAD0AKIZRaGiYzABBBIUhc13SRABAEFhsN4AA5ACiAO43i8AO2uy2+3cYXUkRQGFo8AYeWQ12o3Q88Wo2H8lNwFEU2AYUXSEnwXikyByCXEGCI6EgBSKaAYTGwsxKZSl3Eq1VqkyuhxSaSwTFmJAAHppIOj8OhaNTxPgMGhfEpEDl4NwbVhapAlLhtF5kCp8HgmShmNxImxZtR4Lb+HwnfhuB58LEgyzcGyPMVKB5ePgJNMoqV4dqPA4nC4ADSQY2812QWIetB+EjxfwYXlhC0MLxnDxIBweFFgLYAFgAnMONOZLABhFjh3DIUvMZyuAykiHz5DR+HIXE+JtBEI8ChDZzwMYKDCIJChJPoLBoPAsPL0NhVNCkOtc5hBmUDIbMKM8j+EQzjUhKv6FPwyYvJAADiVDcAgNCQAAspQn4AIrBME9iyOcJDMCShxvogH69m2XjYI6lLnBQPJuluWCnMUHaQGwFCfrBywFAi8GIchHhdCk4GSogCYMPA8wMJAXgqCQvpcNEHGkA0BCWvQ3F2OpsECHKADW0FBipHiHsElYetEpy4PMRpqRavBRie4gAF75Jx7B1vARrSJAXSIPpLrcHxsFmSQ+wAJKhPaiAWhg+A0PSsCMo2CFoEh6geHKCpKqg/hBPA/iaRaMrdKgqbppWhblI215EBgYDYNwsnpIZOZ5kotCTgYSJWuoUZ2j4siVrBG6eek9YUMwSUpe6nGYPALmRtGurpJAyXIGQKiRJphxpRlKHoR5kDYSQuGTUGtz3AQLiRdFtB5oUIq2py8ACE+FDIPFzwWiZnqFaIETyLE37weoAASLRsWgArTE25zOOKkoGuK2ABsg6Q0EQVBupAiTThFGTHvgvJVL50a2Mgy52qQnTyLBf1hd165zp5sRsi8fDGqG2hXt0jKwQjDCGXee6+PlwTnBjVOYORdP8elglVSqXitfSFqRM4WDMLUWUCAGoT7YJ967SW3rC3xOak4gxFZug7LxfwnPSn0CisOwiCVv4eYkMUnsxv9ToFNetrIP4ckXDphxMEojDJRK5N8BV7JDKGhHsMtWBrTuRy22787oL6FrpF21G+Ww3q0NQjb+oGDBsq2oRKAmVJkJJvnhy+bE57YlbvaEjviaIUk0p6JAtzM7d+uyqsEV1Rj6MY4BQGQ9B3o+BDEGQygXGNsxcLw/DCIDki+V0MfKKo6haDoi8mFAcBTH6D5Plv5A41Ee+cH4aCsUuK5+XkBfKgV9NDaF0GAQwS9TAGDVMLciiAAQgxeqvAEq0MAAkQBQBgAIda0G8NINB8oAyzEQRCO45BSEAirt6MA3oBCRDAFyYoYA5SMQ0AuI0HADDRF4QYCwkAkQRTfjvKI/8XBGQYPHUgiA3CHBhJ5eOtBIifTrB6WC8xPrPBqJ6Lk3BaD4GKNrMg2AUA0GmutGGQkSBkHsMUdKCZaBcFgtETE1oGjhlMZlH88VWKvSIKQVRAADJRkQ3HqCCSbQOPN8L3irNaFABplTXCDHJdYvhXHWnLA0YoCAKQuJsNIEgoQrDqhqHUDxJizGERWIYsIJ4AmUGQEE+MZBCmFFwKUqo5TaAABF4D2nwEQSJExLzWgGvaC8iAWLIGiO04p1gymagaJZAAysUpqgjiGKlwA0PShQ17FhdpQAA5LuJZdR7DejTP7bKLA1QYAWPLRs3ZhaJNtPsXq4zbSTOGkGGgJpZLyV9Govgri7QMI8JiZc55skxH6WRSFkAABkkBekKWKR4GhaA4WjOiOiyINBKmrGqRY5KUg/I2O1rmKI6lGzkFYqrcg9hx7ODyF+FgTJCrvKvH3QMLxuUKKVKEppUSaVUH3OzNsbp7TqGAoRXm0o2zSLoN1KcgivA0BxgNdWQYlBdjZYxIy3NagXA9LiBho92D9WkAvM0nYVXIEzOyZcsco5YtEHJbVocA51TtGmfwW5kwmooGavgFrVYyWteIXy2cEDIHmJEScvDogLxgXA/SCCkFckNFSNBqR0iYOwbg7kkREF3J2WQ+5toPbUOrnQ7aJA6H4EFHpCgHDEBcJ4XwgRQiREf3oOI+Qd4pGYBkXI3sIZTVXLyAXC6QSIpTtDa8PoiB+mDOGZSSAQTsUNshc21tzgO1GkifY5ATUaG0odWO3sC57ABmwdmag5RkE/iCScjQAJU6msaqusA1IN0nMiepd9n7v2ht/U0oD+xzROx6HwC+Aduwghkme9iigR6qqMOqpEmqd46u7ocfVXrM5BqrEaO4oaojmpaJGqsswbWyIMOacgya+EGAgGAIwGas3INzbQfNeoi04LwQQ8t2yqHkJrVQ8DuBIOfX/QM5Dx7uEpv4ZYPtpB35dyHZIx1E6x6EqiImydad5yLEgIUthbsKGeQXUu3AK6mnruQ8B5KoR2r5noI2P8HJKCxDlMyD0sm+J/mQHmRs05VkADUvJlruuxWWtN2LeHEGnGdiVKSl1EsGSjHmuTY2kP7XgMIFisTwSQSsapPpRF89iysozJrLlCJQLkn085ScoaEGU9BTj4H0rqkVcWSwYrEANSsUXYvVevBKSsvmJCypoeNqJsn+AJm9VeBLDhuB5aiIG4pSMMu+VybY2CAHkNre3mvBD3YDkNapMTDqn9Jl6WFgm4LDnQurqiaEviq3A2pZtgZ1bVsiuUgFEKKI02QqHEm8N9AiBZDKpzejMYlYFuqyWxBML936AXsjBBV4EUjt+mIXUBEYx9gAHks4OcvJo5ry2MdwzCyUO2hQlR0tp3lusioxu2lx0GTrnl8cXF8wjG5Z9KjUawA4eUYOPQBfPOmBL1JxJyVkFeyoA8Eoj1pEa8Ri0/t04NhCaQedztDMgETy06ULj7Jl0LhunkJckC4NMwxfETPSlDPgFIl5OSCluceUrvswiNqMjjgO/3pCA+SeUeMRr1IhtCErnauXTVA4MJiCgbXLFUiZZKEuVFOqMGoFIvicPocQVa7UZ+kw6dp/TP7Dph2muMlr7Ga50hBeeZy42AxRjuwpAj3D8xPMUJMv0h16tXXC6xULt2TM9BaRtgUjD0z06Ss+1YqM/b7Q+Kd5tgcLFSmrf1gSkUWgSO0BpFpENJsrcip+TwAQPmF02EEdVvpDwUiW0zclBM39gNAZ2Nzy0rFX15EL0rBTz4gB01X9g9C7HuAgjOzPyIGVhSQcT0RPHZTV22j4nwRPAgltCIHwD4laQ2yzyp1hEmRGmjln08jbnSgcAjlpUOHZklSXyMhC2x2+1fUgDhyAMrGL2oj4ht1bCUGIIwKO0SxpnTlmAa0R2R1tHRmJmtgRBgNzw9F+1myiV83iDoFe2n2w3Uw1S1VI0Iw9QNSoONQo1NWo3DVoytQYxjSYygExAY3ZG9yCVk3k0QEUw3WPUiSUCM1oC9kIlzA33I123oCszEBs2k1CHszyyc0+hcyGVPRVBzD+G4UgF0G3VaQwC4H1kFASAwCCQMHyKgBaQwCpxbmnBVS4ESCKJKJbU1kyEgAAF49BIBYZaBKi1MON00DJeMc0aA810FhMS18Ey0iEcoFwARhcZMHN/CljEAJAwB285MiBsBphMBeQVNu1U1e1hEtNRFB1HBlwJER19MeoVgw94jU8+cZVfBljkjpwNjhhahms4I9iq419IkphZguR8FeRdpGRJDRVdj8wmVfILohDzwgsppqBDtO5I4SpXZZMEtspx471aoKdsw9Ft01RulNQPghh1BEBIkUZeY+Igk9QPgyAiBrgRkHsGS0AjQmSJRWSVg1gmkZ8p0581cYk/RGgKBHkIIaQVpuwJRrxY5apcBZB09SJyIx4nQTw1BUDYcYsFAqJmArwuA/w7Q2A8ZE8JkvAMgYCYVfBEgJYAZaArSolSSNRLkWjXQLSiZqogwXSelZJmSUkDQ6JeY71MZKA7g2D6Br8TSaRfkOUfxYIcjj9VklTC9jlkAotVk+izxG0E1QZcAdEXhTTYQQ539FcXp7BFpKswhjRQgmBuwKBBdjDsYSEV9BRag85Z5QhvFp4PAoTn9ozb9YyH8BDMB7YqBh1kw6IER/YEgpF48pR65zhOV3ofAFlzhlTFgxTEBlx9x6zdYoyEpBTbNcoE08R6ZZBHEFzoBLySBVlnRaxeIIIxywyKAAt2wLokyoklSrymhGwpV+dBpEius1UzDcMLCk8LQztPVDUCM7wU9HDPBLUo1XCFh3CzQw8EK4i5znjpULTgK7NPiJBviUTcA/j9jASFyvzXz3yPAgkiKSLfj/iDiSArA9FqSx55hyAtQSTRiZFs0UFJiC0MEsERNS1CEK1JNGCVi8s1iGANitifjqBiBmK19gi2NU1hiuNYE+LCE+MJiBMpjRKZixN5iSFFj3jEE/Cwsv1ViUZ2Aji1NTj+0dMriAFbib0mMkQHjWIniCKlRRMKRgTA8wSr189lFT9YgWxPIbdXd3s+BeDJRWcItBDdS1oxz0EOV+NVdmx0hfJrh/BmQXj8LLLuEDANh9gAAqSqxdPLNi0PYoWcXNXAaqrgBFUU1Ub2MrIyabWrfg0GSLXUkzLbGZKUVUz8b00acyoyL0QGPq0VKSIMVbVAb6SATXOs+5MIysesHwQxKIYSY5PgehCkLs6I3qvHVdbqYkSAaq2q01CKR5cQe0JqiYlqyqrgeqjnO9WCdIfqe0DPUNewUUWxb09IOiBiAjdSJqYfbzVK2LYayAKKLLEvXyGEgE9sbcHU+GpS0Ib030zUf0nk8oIMzkEM+vKUb3aGv3bzXIfy54dzdAeXV0MU73QoSIQCsvHwYwqJf82oexOoPbZsbqAAZiqpqocxzzaxevYFarRSQA6rHM73YiKzVPRLCtWyb0DUrCUAC1SxCgtEbBzBDG7KvE5Ahu1N6CKHUlW0GvhqRK3Smtz0BlBWa26kxDlGJuku6yejCFvPoGosNDfMC3isYC9TlW/NvPsDQHiCVOPKSN7KuXojEHTHoG9OvBDApH6FWQAA0gVZADZya9RTxfBkMaRtqSYnzJRox+y+JsD4xcCUJ7KlQHdDkAbQhXcA4j9QLe08NbD3Ux4bDLD4L7CqNrskK6No00K7UvkogsLec8Kfk3ivbt07rQ16qd9pbZgsiUkA6tVaKV6HN16ytN7Ol2KaSXo6SIIglzq0j/hFkataA0iABtAAXTZPoF8MQFXo6DaLKMwCCUnCgBnvoDnoAteLpoPrywer+q8BPu3vKF3v80C0gfuseqUxPrYvjA4tpN+qvvxrqApLaDvoAG92J0huSWTYAuBVhmB1gKAABuRLLkgMqhvk2hooAAXyCUrCCRM1eF9xSEKViC4CEap2PjEGADREiH4ZhqRFyCEcgAAB8+SfA9BuGolaipG7zRsf68YTMuAtGiYejrBvwkASBgB+i1HAHBErRZ6R6LgwGSrl6UjTVJbag4GqL2K/Mg72wXHQ03GKAMGz6A9L6q6PTF7t1xqSAAB+d3V6CUAB44tNbSnjfi/S1BIy4tQKiSiTCyr2qy1Ymyvwi/LVRyntDTM47eAdZoMsScuOTyowbyhlSzHC+ewC/694lB5dVdb4y/CgIEjGEE7kcEtifsqUS3QAltLVBc/uN/ekH0A8tiakKKrMJUOKoyHzLEuncpfSWIJfHEpmu9AO6kfXaI+uJ23KOnV3SsOXGqeK5rKsbQz6KrLqsPCrEQz7CCCXAoQXE7Z2H6unVAchLawuUghckVVRC6ffQ7I/CAli6A+nQqPUGQ0ZKmlIBEZmBgoUzyA0b5QaC8fspiRMGCWAQq+wCSJTKs+gEpooBwAQSyvyaXVuwWbvMqmoxi6gAJ3prVIJNq+WjXMUrsGkTNJFaIMfQiCfEgBoKfHm+FGtPZV/bcC0ffFMHGx5vPV3axoJI+32VCRQEgblygXlyAVZUasV+FhoPFJqhnRGhzBoFosFb+jFjQDQBoAEGITEexhoZuVeCg2Xa5AoImWZ0OGfDAG14NrOZAfAyFSYZMRKlAQZ4mQrKoaPZMTAeQc6w6hNq5c8XwUHKoDFjjFetB+0Q1/pj6rkJ7MUsV3U8fNg6V1qWVsVvrKCf8pEhV9oAXEoBAec1AaN9PIgy2ZwAAt2j25oel5emUZ6K8G8F3VM2uh7CN7M3A0hQXBmRVGlhDC+vKvgFIIQAoO9DXA2UO9UXyQcu/OMluy4EPHfdDJQTsoZFwhwQNdAHbHAgZFCJd9Z0ZNJBSMOZsdm+mzfQG/Nxcb0GRTA4mzWWMVlDbSRW0XkCUv0HWC28dyy7qWDWsygIC+UjwZseIMQLcXd5QqRFHAoWSR9mSZwCiEKnkOgRhqa7FpUXEcOeQamBBa3YnGu58h7IVOsl7AyGQJl69pM7ujTXuyw/u4jWCn1Ye2IgOCNFw8IKeot4BmIhw7C6zRxiJzpvxxzHpqZo1zxrB7x/evT2+stzBv4IErAXw1Yzd9SpJrS7jXSxBdJoSoTYy7J8TBYqtJjxY6y1dL9VuI0Mpk4iplyi4XTDyhOLynylp6zXT7+2+jIzdYK0Euj4qR7fMcZ9A0z5Bi6eN1nUcuG4bA5msO9FpFud+wouoholVSJL89SFGLkXwKkvopAN6c8OVSKQO/Fv5dQdMqUXhpEmR6moR4DKChpQJeHNF2gcm/wV6BRc5Mky5e0/wTceMllCgWERF3YjbOOufdjmRNu2QgqgMIgGqRgA9zlPrQyIJGUZL1dFMvIdR3JGkcoEVc5T6TQm9srX7kApF370ZJW9Zu8AFnnUDzF0/DdAPTyc94cqZQsKRM+YpTMU7UlmxRLNaHfITg5a9iH6dV3LgF8kt3wObl2vGPT6Bp62BwS3AIJJ0w2151iUWYd+a1RRIPTnVxq+nxnwXRWp55W98T8C6bYhN/sZALn7+gJuBjIC3PL8ICkGKEqfROkRmpgcnCUC8dSfwPZ520aZ3NZ7vPGcKIgDQSsaIb+6IS361pFtum3mIb++GQHR3yyB6zX/wBIjWnG6IeXxptRPpiB6XhzCzwz/pomYE7LMKvvX/F6VrkOwHqaaI2TOFtfBF6Fw/J525B7fxQJL3e2ub4/UkCZ+GYpDGUMvr/6juk7cgXbwb1AIo1VE/CBqdsZfqCJjctMulWgfd84f6WH0oWgFJH1qkP17t4peDMxbNtabfbqirbqGg5QHweguP/z+jWkP4bwdXjRPELN1bHZvZupNOk347hQ3ASsG3UmmdxiQXV8z/aMCF4rSt6YWuu97f3dh7fwAoCPVbCZsT8wvhh9RScYKthOThpwU7OEUKynW1Kp1sYgN7GfIQOmZye7OZ0CVnDinjT0SUA3ChRFuL/Q6I1dai9RMgI0RvTNFWifkdouUSMa9F+iiTIBvAPU6j0EuCRbTkBSS6h9V0qXeBsTD+BGQfydFVAekXQFn0NKyTFzvAjSbjEMmwlaYt5zMqVoli+TWyrJRsoygwAgXJpGAE1YLhVM5TQRJU20xRc3KNxZMKOli5yIVqYeb3JjAy6kwpQflO7tuke5cCmkL3GgG5kZBn8koZsdlD+0o6go26X2UVCV0RJBUHwD4YSllQMrv8k0iNUIF93hxs0T4XbKvFyA0J6F5si2TOOgFAhg0PMFyNsqbSoCYx/YIOFnmux1DJ1/qq2SgrkO9KVAkcpHVQuR3vxeBjCxfQ4M4JhQRgwaKWTVC6ApAd0FsuBSFIM2yxgEt8BWQNMVnZ4XUmkLzBqne2rKNY1WStPZh+CzykhnBsfMUiq3YFsQuwCQXdvuDioC9OaHQgyGxFA5BhwhWUEjqS1aGIA0c2ZTHATiSrfZRkreS2CzxWEBxxeStO5FryICU4EhnFPKs/FO7tDuaDIduiQEKygRMscua2JwSiRp4Oebdf2KLl8ji4A2T0QXB7j9gdhtctSOYLGQNymCjcqHGPKwQXDQ9oRtoBnLkMOF59RUhPQGneGZwLDnmPuS9OeldjfMfa6kUDsDwexJCj8C5AgKSO+h65SMnyPqOAyf6L46kkBdfDSLpxz9fYERH4RBEBHZ9BcyBAAnqjy4n92U++POM4JV7NAdsmeZgW3h9B8g6c8Bb6haEmzdRvK5CPAMZjkhF4x++uWNGmwwDyBWc5KaxLYlkxRBEg1YEvDDh7A5t9wooiUPLxb5J8fwELKsGUCKAp5/2dI8mkaPypEY8ui1ZalGxrTR4S2mWDkbeE5ilBCgloltIZEW5sg+YkyJsJEAWzG92UowgZOMJ+wTAVEIdUqGtBtzocjAS/CVF4FX7rRGxG/FgtvyrEcE9+qsAjrIGOFGQ4cvmZapRDEJ6j7auhGQtyMzgfMecxoUQHgGWxK0DxNzfEXyLpCC4XR9ogAeBSAF8wQBg9I1OAJYE0ZkK9GGAehTU5z0aKyDczs927wYDeB19IoYQypKRJEgjJFhpWGXDMMiaRMUZLUWnC3YSANJATsLGsaATEBvWWcYcIe6FBhBiADwSQESBJkuA5EyiRgKyCQBSGLrfYFwybABpJSReK5ib1GRP9xBznHSlIL0oyCPOhaLzuJR87mU/OJ5RYhoL/BgAz+84DtPoPC6GDIuYiUwXUwsHjo1whwUiSQGnC5AaAaRSJHdzOTbpRQr3BHM0MuHc1DhfqVkOyDQw4isuohBUrxVW4DEvICkHiu13SC4hChVACuJQF664cUwxVLtrX0uH0lLJNAJELk3UawQoJnkxrtgNDTyAgWtoPFnGQ6RcBFqSU/Bl5OnJSlNoxtP5A91bhcU6AAzCoGTC1AEQUgjDBKPBjrHVlEyL/UvMlNdJeSMcuEVAD1mIhAsVUd7TDL4KzaxSBoFHFDGxHNKL0LwbkrKLVO6kT99QRkhdhr3MrdQe6EFSGlBSIygCh6waQiZAN/GT1YBUAYYBFNlzwB6ojk/IGr10ZBImhbYPGKQxNLVkFUsKOsGKGbyQAOGXANIg9QCm0CTGLAMxhY3IK0A1GW6fSYZLFAkATJkAQAEmE26Z6TJESBvTb8H0m0lViKHbUfpL7f6dbj6BAy8AIMzBmkEKAQzpg0MtaLDKMkIy+gNnXikJLc4iTDKcg8SbMRya+dlB/nRBHJL6AKSks5/DtIMXYycZJBmaaQYJQ5mecsmEkxQVJX5kAh5JTWMLmBSMEXEam1xLSXcVJBBI0ijFc+uvzQxYdW4WXWaf12RplxVQRQykAFO8leBihgeNcs8ni5BIiR9RN0JElKIdE+BFuZsI+E1RZsO6viSkDAyrJjTCpTs6lqDB3RBzUs0We0MEAAaCJYg0zNMXxG0k95VWU0MvqGTvQd0QButEOX+DzgLoMAAUqKIRBNkyTGg56B6WFVxK1gxyVs/6lV1eKpT4wVUHtuUCilm1qyIpAVnEmiDulXiGQaVsCiuSblU6KoRsEWTj4Nll2vYpNEYAepBh6KDMo2T8TrlJFcZnkt4fsVxhoYRIiwWUkQDCnGgbwh+G0kfKWzRh1gk0S2ghjWkfIW+Mco8bjDb59BYQYAQHvtyhz2z1u5/cmsPLQCxJJRKVaUf31lHSRLCi1L+ZNLTxdC6KtoO3lNFMkDiigaGW4HJBGaNCSgiEK8jCOuEhJsFcMvIIjLhE8B/MPxXwVBCQU+on5uscHP1jOYMzGaboCuV7PCYVE+B2bDpN3HogeB1I4C+mIcBjn+TAwcaX/JwvVkTooIOcv0FaCWl+lpF0UB7EwtlxNQecUNV2BNK7apxK2yJH8Cq3WDkonI6ADOUUDTHHiokZAeOO2FHFh0lSQQ9ub4HmA+Ss8OGCTpBT1QHTPxR0+Tj+InqoVzpNjUvB4oEWeyWI3sgaLEyoF/1+FIGHeVNAgn5d2wdMtWT8Uc5QB+Gl6D+mkuYCRJ7JN0/1OmGhiHMPZvCn2QHLgFRK+FHcjxYkr9nlFu5zUEDKTNwA1ySlEDbJULPVmcIGBkSqIM01AZXTt0lC4yX0GNnmTXyHA3CfdxFTTLGZRnGhV/N8ifTfAoyGOSwvZDtCYpDMkZQUq7hbz4ZxSveXPhFHqht0tSgaF0WIaDy2JqLKCOQoLwGTt5TMhHLVwwV9KIW/EyWYJOlnCTZZgmMSQrO5mSSlBllVWULOOrhROEKkzWepMuK1M9MDTXSaIojw9AUgRQZslyEVBtll5aGHOXHM5TYwIFbEd7jQE+TxdogXSbqQ0HrKOAsAp8pgf3V8wIr1oCQKQlEk97SAKEOWbFH4EMSoKqwd8qAl4HMkAoNqy80uqhiaB688qTiafuHIcSqwO4tKWspcHmkksX5M/DANyO/LyIheUTTMc7HnAEZog2ylZHwAMS+RVqVq6MNEEKmutBE8XGOVKobkch7BBYFULiw75AVn8sqmeWmXymHAtxgzY1aMlghK1zVC02ZG6oaCkgvVCkB0PywgVjS/w5kmOQ2E9CVBygi1FQF9VthKLHUGkNRQTUehddVYbi0ZDrHZCUrVgXqdVrUDABXsu+XuD0NsudL2yvFzsnxWBT8W7SAlH4uCsEogGhKlOjGO1LBmUVsQ56066AeHWk5UEK5aRaAI2kiR+VOmDkgNH2U+l8w+cKqeeEMSBWpNQV/GcFSJUhWmVJKixJ0D0GXDKSkmzlc4tU2i7mD9ZekwGVXLwDAZI6OtX6ifKaB4JMMrkncQqXdBNLfAXUnpKUoWDOyBBkdT2fE2GSDSVFeLFUcSKSl/qApkSQQCIASLqQPFF4WkLnnGBVrLkvUrKBfLClxqjQN8q+u9PUZBJtl6jdCcnGkAdKcBaFLaeJx2nAC9p1hEjEEvtGIVFOK6twtPSYHRKciOA+QPBs1CJLipm6funPUEHbp8NAGmGZerZlgrMmYlKFUrMfUo8X1C4cWZpQvWucBK16ozSZTmIPqBZ4gX0OsU2IXjfQr6pyhFw/WuV0VMXHSQbNKQP1EZwE9sGbJNCrwwqZGjKVBsWnKa6giGnyXSuaYkTNljKhDT6rsHDMwqmy6jfN0Zb49owkQImpeGDJlCGsu1UoDXjKkFa/Q4WRbFEmuKBQSCnMaNU1ojUkB5ABiMkcrQWSCwKWXi18L9WYCOAA4yEoYONtK2UN9gmWgmsC2vluKUeb2PGISi1Tk0aGdDRAGhIdiX5vMYpaAFTl6RU4X8dZI3kDHzjsBqMeAPOOKwzp4FDEGAGGm01A2NzClbEBacN3m1Ja9SrKqoR4Dhwp5Xtk0tDOBuG2fb4tPpftUhp4qMb5OLo8VekJJhFY+ICqn1S5JuHs9odh8gQrBHhCXIWVBpY1GO2YSpaw8QSTZbfVKWTL2VpeGhZon76bLSpPoAUdEWvnnB6Smy6FKzpp0L0MAq/LAMzvq0uwudOQmgD9oGJITnArWz4QtSNVNbGhWAeNSrU/CLVP54u5BaztDauy6w3BOmVTtXSRIAoLoFbstO0UoB9VEqn0Nm3tC5Br84ORbBXNIC4Bk5mOW+tAHwDf0+dHNZyU3Mg3R8cdy0gdXDpuAbIJSmIojYDCHXbTXxuqaCuOtk6TrvxThU6eEoAlybYNHk7qapvQ28b0pVhbdCFoORhbkByDOmfprs0GUb18gxWc5tVmuayEClTzTbEs34SmB4y46elvF2sUih1EooXE2kJKMlUwG7igfO6kwS9BTEshhgAobXBqG/JBhkwzn2sMttnDRiaQ25EEDyijDTvLnqH1sS6Z8lDzY3rFnt7S8ne+Tt3veEzKmkiQMLB9XmHP0X64+npJPpIYz6V9C+9hkvuQlf62GdDP6YxOL10Bn9zMoJMfsain629RbU5VF3KV3SjIQSF3W7umAe6vdDme/aukf2ha+gr9DIJNxtmxxEtXk2RexJbGR7xG9I2A/7srK3SD1SBwfM9uppEVoAErNgokAIOQ7A9SUyXefX1JZwjk92yVtYzgNiIEDDBu8Oxvsbf12kgOO0rHgQI4GS9eBl+lwetpQ6SDzM+HRALhxN1NANBj7fusqVSHQOk2LRokH0aQAtGFkDAKsh5DWxmiD+++iocoD4HuidAyGbYYCbNEQZ/RdQw5Gx2jIvtWhrbnDmYRA5z1kCaBCvAezrxX4fm3eKzH3g/w/4mkwBAoCkIqA1AYCW+NEfvj5x1AAAfWmCIAijLPOgEUYRiA1F4BgAowAHZhao4DYLQBHAkAAAHAADZhaJAAAAy0B6j9RkgKOGFpoBhjNiYWt0Y2DC1Ygw4do+0YGOdGGAWwWIPUYgSGACjJADYAwAEC0AAsAgYcK0ZIA9G0Ao4FY2cdHCxBejGwUcPUa6PDgAArGgGuNzHOjWwFIGsdqMFGNwxR0o+UYaqVHV46xoAA -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=toeverything/AFFiNE&utm_content=12112):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: JimmFly
created_at: 2025-05-02T09:51:46Z
body: * **#12112** <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/12112?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/toeverything/AFFiNE/12112?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `canary`
<hr />

## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/toeverything?ref=merge-queue-instructions-in-stack-comment&prId=12112).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>

<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-05-02T09:57:48Z
body: ## [Codecov](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12112?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`canary@1426a38`). [Learn more](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#section-missing-base-commit) about missing BASE report.
> Report is 2 commits behind head on canary.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##             canary   #12112   +/-   ##
=========================================
  Coverage          ?   54.16%           
=========================================
  Files             ?     2599           
  Lines             ?   118382           
  Branches          ?    19182           
=========================================
  Hits              ?    64126           
  Misses            ?    52694           
  Partials          ?     1562           
```

| [Flag](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12112/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | Coverage Δ | |
|---|---|---|
| [server-test](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12112/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `77.55% <ø> (?)` | |
| [unittest](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12112/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything) | `31.17% <ø> (?)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/toeverything/AFFiNE/pull/12112?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=toeverything).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-05-13T02:42:20Z
body: ### Merge activity

* **May 12, 10:42 PM EDT**: `JimmFly` added this pull request to the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).
* **May 12, 10:58 PM EDT**: Merged by the [Graphite merge queue](https://app.graphite.dev/merges?org=toeverything&repo=AFFiNE).

author_association: CONTRIBUTOR