diff --git a/.agent/rules/frontend/frontend.md b/.agent/rules/frontend/frontend.md index 304ce31a54..4df2520b2f 100644 --- a/.agent/rules/frontend/frontend.md +++ b/.agent/rules/frontend/frontend.md @@ -49,7 +49,7 @@ Guidelines for frontend TypeScript and React development, including component st - Use React Aria Components from `@repo/ui/components/ComponentName`: - Search [Components](/application/shared-webapp/ui/components) when you need to find a component - Use existing components rather than creating new ones - - Use `onPress` instead of `onClick` for event handlers + - Use `onPress` instead of `onClick` for event handlers (exception: Dialog close button uses `onClick={close}` from render prop) - Use `onAction` for menu items and list actions - Use `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -58,6 +58,9 @@ Guidelines for frontend TypeScript and React development, including component st - Colocate state with components—don't lift state unnecessarily - Use `useCallback` and `useMemo` only for proven performance issues - Throw errors sparingly and ensure error messages include a period + - Include appropriate aria labels for accessibility (e.g., `slot="title"` on Heading in dialogs) + - Disable UI during pending operations: `isDisabled={mutation.isPending}` on buttons/fields, `isDismissable={!mutation.isPending}` on modals + - Dialog sizing: `sm:w-dialog-md` (simple), `sm:w-dialog-lg` (4-6 fields), `sm:w-dialog-xl` (complex), `sm:w-dialog-2xl` (extra-large) 3. Error handling: - **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors) @@ -69,79 +72,103 @@ Guidelines for frontend TypeScript and React development, including component st - Use `isTouchDevice()` for touch vs mouse interactions - Use `isMediumViewportOrLarger()` for desktop-specific features -5. Always follow these steps when implementing changes: +5. Z-index layering for fixed-position elements (don't invent new values): + - `z-0` to `z-20`: Content layers (sticky headers, table headers) + - `z-30` to `z-40`: Navigation (top bar, mobile header) + - `z-60`: Side menu collapsed + - `z-70`: Side panes (backdrop at `z-[65]`) + - `z-80`: Side menu expanded in overlay mode (backdrop at `z-[75]`) + - `z-90`: Modal dialogs + - `z-100`: High priority modals (nested, confirmations) + - `z-[150]`: Toasts (always visible for user feedback) + - `z-[200]`: Mobile full-screen menus + - Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context + +6. Always follow these steps when implementing changes: - Consult relevant rule files and list which ones guided your implementation - Search the codebase for similar code before implementing new code - Reference existing implementations to maintain consistency -6. Build and format your changes: +7. Build and format your changes: - After each minor change, use the **execute MCP tool** with `command: "build"` for frontend - This ensures consistent code style across the codebase -7. Verify your changes: +8. Verify your changes: - When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect** - Fix any compiler warnings or test failures before proceeding ## Examples -### Example 1 - Component Structure - ```tsx -// ✅ DO: Create focused components with clear responsibilities -import { Trans } from "@lingui/react/macro"; -import { Avatar } from "@repo/ui/components/Avatar"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuHeader, MenuItem, MenuSeparator } from "@repo/ui/components/Menu"; -import { LogOutIcon, UserIcon } from "lucide-react"; - -export function AvatarMenu({ userInfo, onProfileClick, onLogoutClick }: AvatarMenuProps) { +// ✅ DO: Correct patterns +export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) { + const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen }); + const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline + + const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern + return ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ DON'T: Mix unrelated functionality in a single component -function BadAvatarMenu({ userInfo }) { // Bad: Mixing menu with logout functionality +// ❌ DON'T: Common anti-patterns +function BadUserDialog({ users, selectedId, isOpen, onClose }) { + const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values + const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated + + useEffect(() => { // ❌ useEffect for calculations - compute inline instead + setFilteredUsers(users.filter(u => u.isActive)); + setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types + }, [users, selectedId]); + + const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need + return `${user.firstName} ${user.lastName}`; + }, []); + + const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log + return ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.agent/rules/frontend/modal-dialog.md b/.agent/rules/frontend/modal-dialog.md deleted file mode 100644 index cfcac3f518..0000000000 --- a/.agent/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -trigger: glob -globs: *Dialog.tsx,*Modal.tsx -description: Rules for modal dialogs using React Aria Components ---- -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.claude/hooks/pre-tool-use-bash.sh b/.claude/hooks/pre-tool-use-bash.sh index 709d2fea04..1128d2e071 100755 --- a/.claude/hooks/pre-tool-use-bash.sh +++ b/.claude/hooks/pre-tool-use-bash.sh @@ -11,7 +11,7 @@ cmd=$(echo "$input" | sed -n 's/.*"command":"\([^"]*\)".*/\1/p') # Check the command and decide whether to block it case "$cmd" in - *"git merge"*|*"git rebase"*|*"git cherry-pick"*|*"git reset"*|*"git revert"*|*"git tag"*|*"git clean"*|*"git push"*|*"git remote"*|*"git config"*) echo "❌ Dangerous git operation. Only 'git add' and 'git commit' allowed. Run this yourself." >&2; exit 2 ;; + *"git merge "*|*"git rebase "*|*"git cherry-pick "*|*"git reset "*|*"git revert "*|*"git tag "*|*"git clean "*|*"git push "*|*"git push"*|*"git remote "*|*"git config "*) echo "❌ Dangerous git operation. Run this yourself." >&2; exit 2 ;; *"dotnet build"*) echo "❌ Use **build MCP tool** instead" >&2; exit 2 ;; *"dotnet test"*) echo "❌ Use **test MCP tool** instead" >&2; exit 2 ;; *"dotnet format"*) echo "❌ Use **format MCP tool** instead" >&2; exit 2 ;; diff --git a/.claude/rules/frontend/frontend.md b/.claude/rules/frontend/frontend.md index 6316ca51f1..cab5791023 100644 --- a/.claude/rules/frontend/frontend.md +++ b/.claude/rules/frontend/frontend.md @@ -50,7 +50,7 @@ Guidelines for frontend TypeScript and React development, including component st - Use React Aria Components from `@repo/ui/components/ComponentName`: - Search [Components](/application/shared-webapp/ui/components) when you need to find a component - Use existing components rather than creating new ones - - Use `onPress` instead of `onClick` for event handlers + - Use `onPress` instead of `onClick` for event handlers (exception: Dialog close button uses `onClick={close}` from render prop) - Use `onAction` for menu items and list actions - Use `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -59,6 +59,9 @@ Guidelines for frontend TypeScript and React development, including component st - Colocate state with components—don't lift state unnecessarily - Use `useCallback` and `useMemo` only for proven performance issues - Throw errors sparingly and ensure error messages include a period + - Include appropriate aria labels for accessibility (e.g., `slot="title"` on Heading in dialogs) + - Disable UI during pending operations: `isDisabled={mutation.isPending}` on buttons/fields, `isDismissable={!mutation.isPending}` on modals + - Dialog sizing: `sm:w-dialog-md` (simple), `sm:w-dialog-lg` (4-6 fields), `sm:w-dialog-xl` (complex), `sm:w-dialog-2xl` (extra-large) 3. Error handling: - **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors) @@ -70,79 +73,103 @@ Guidelines for frontend TypeScript and React development, including component st - Use `isTouchDevice()` for touch vs mouse interactions - Use `isMediumViewportOrLarger()` for desktop-specific features -5. Always follow these steps when implementing changes: +5. Z-index layering for fixed-position elements (don't invent new values): + - `z-0` to `z-20`: Content layers (sticky headers, table headers) + - `z-30` to `z-40`: Navigation (top bar, mobile header) + - `z-60`: Side menu collapsed + - `z-70`: Side panes (backdrop at `z-[65]`) + - `z-80`: Side menu expanded in overlay mode (backdrop at `z-[75]`) + - `z-90`: Modal dialogs + - `z-100`: High priority modals (nested, confirmations) + - `z-[150]`: Toasts (always visible for user feedback) + - `z-[200]`: Mobile full-screen menus + - Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context + +6. Always follow these steps when implementing changes: - Consult relevant rule files and list which ones guided your implementation - Search the codebase for similar code before implementing new code - Reference existing implementations to maintain consistency -6. Build and format your changes: +7. Build and format your changes: - After each minor change, use the **execute MCP tool** with `command: "build"` for frontend - This ensures consistent code style across the codebase -7. Verify your changes: +8. Verify your changes: - When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect** - Fix any compiler warnings or test failures before proceeding ## Examples -### Example 1 - Component Structure - ```tsx -// ✅ DO: Create focused components with clear responsibilities -import { Trans } from "@lingui/react/macro"; -import { Avatar } from "@repo/ui/components/Avatar"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuHeader, MenuItem, MenuSeparator } from "@repo/ui/components/Menu"; -import { LogOutIcon, UserIcon } from "lucide-react"; - -export function AvatarMenu({ userInfo, onProfileClick, onLogoutClick }: AvatarMenuProps) { +// ✅ DO: Correct patterns +export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) { + const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen }); + const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline + + const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern + return ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ DON'T: Mix unrelated functionality in a single component -function BadAvatarMenu({ userInfo }) { // Bad: Mixing menu with logout functionality +// ❌ DON'T: Common anti-patterns +function BadUserDialog({ users, selectedId, isOpen, onClose }) { + const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values + const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated + + useEffect(() => { // ❌ useEffect for calculations - compute inline instead + setFilteredUsers(users.filter(u => u.isActive)); + setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types + }, [users, selectedId]); + + const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need + return `${user.firstName} ${user.lastName}`; + }, []); + + const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log + return ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.claude/rules/frontend/modal-dialog.md b/.claude/rules/frontend/modal-dialog.md deleted file mode 100644 index fdd1aceeb1..0000000000 --- a/.claude/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -trigger: glob -globs: *Dialog.tsx,*Modal.tsx -description: Rules for modal dialogs using React Aria Components ---- - -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.cursor/rules/frontend/frontend.mdc b/.cursor/rules/frontend/frontend.mdc index 4e3d1b47a4..6e734568e3 100644 --- a/.cursor/rules/frontend/frontend.mdc +++ b/.cursor/rules/frontend/frontend.mdc @@ -49,7 +49,7 @@ Guidelines for frontend TypeScript and React development, including component st - Use React Aria Components from `@repo/ui/components/ComponentName`: - Search [Components](mdc:application/shared-webapp/ui/components) when you need to find a component - Use existing components rather than creating new ones - - Use `onPress` instead of `onClick` for event handlers + - Use `onPress` instead of `onClick` for event handlers (exception: Dialog close button uses `onClick={close}` from render prop) - Use `onAction` for menu items and list actions - Use `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -58,6 +58,9 @@ Guidelines for frontend TypeScript and React development, including component st - Colocate state with components—don't lift state unnecessarily - Use `useCallback` and `useMemo` only for proven performance issues - Throw errors sparingly and ensure error messages include a period + - Include appropriate aria labels for accessibility (e.g., `slot="title"` on Heading in dialogs) + - Disable UI during pending operations: `isDisabled={mutation.isPending}` on buttons/fields, `isDismissable={!mutation.isPending}` on modals + - Dialog sizing: `sm:w-dialog-md` (simple), `sm:w-dialog-lg` (4-6 fields), `sm:w-dialog-xl` (complex), `sm:w-dialog-2xl` (extra-large) 3. Error handling: - **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors) @@ -69,79 +72,103 @@ Guidelines for frontend TypeScript and React development, including component st - Use `isTouchDevice()` for touch vs mouse interactions - Use `isMediumViewportOrLarger()` for desktop-specific features -5. Always follow these steps when implementing changes: +5. Z-index layering for fixed-position elements (don't invent new values): + - `z-0` to `z-20`: Content layers (sticky headers, table headers) + - `z-30` to `z-40`: Navigation (top bar, mobile header) + - `z-60`: Side menu collapsed + - `z-70`: Side panes (backdrop at `z-[65]`) + - `z-80`: Side menu expanded in overlay mode (backdrop at `z-[75]`) + - `z-90`: Modal dialogs + - `z-100`: High priority modals (nested, confirmations) + - `z-[150]`: Toasts (always visible for user feedback) + - `z-[200]`: Mobile full-screen menus + - Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context + +6. Always follow these steps when implementing changes: - Consult relevant rule files and list which ones guided your implementation - Search the codebase for similar code before implementing new code - Reference existing implementations to maintain consistency -6. Build and format your changes: +7. Build and format your changes: - After each minor change, use the **execute MCP tool** with `command: "build"` for frontend - This ensures consistent code style across the codebase -7. Verify your changes: +8. Verify your changes: - When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect** - Fix any compiler warnings or test failures before proceeding ## Examples -### Example 1 - Component Structure - ```tsx -// ✅ DO: Create focused components with clear responsibilities -import { Trans } from "@lingui/react/macro"; -import { Avatar } from "@repo/ui/components/Avatar"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuHeader, MenuItem, MenuSeparator } from "@repo/ui/components/Menu"; -import { LogOutIcon, UserIcon } from "lucide-react"; - -export function AvatarMenu({ userInfo, onProfileClick, onLogoutClick }: AvatarMenuProps) { +// ✅ DO: Correct patterns +export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) { + const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen }); + const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline + + const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern + return ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ DON'T: Mix unrelated functionality in a single component -function BadAvatarMenu({ userInfo }) { // Bad: Mixing menu with logout functionality +// ❌ DON'T: Common anti-patterns +function BadUserDialog({ users, selectedId, isOpen, onClose }) { + const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values + const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated + + useEffect(() => { // ❌ useEffect for calculations - compute inline instead + setFilteredUsers(users.filter(u => u.isActive)); + setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types + }, [users, selectedId]); + + const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need + return `${user.firstName} ${user.lastName}`; + }, []); + + const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log + return ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.cursor/rules/frontend/modal-dialog.mdc b/.cursor/rules/frontend/modal-dialog.mdc deleted file mode 100644 index 7e8e6f7da8..0000000000 --- a/.cursor/rules/frontend/modal-dialog.mdc +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: Rules for modal dialogs using React Aria Components -globs: *Dialog.tsx,*Modal.tsx -alwaysApply: false ---- -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.github/copilot/rules/frontend/frontend.md b/.github/copilot/rules/frontend/frontend.md index 5ea7e5ebed..8d8fbf65ee 100644 --- a/.github/copilot/rules/frontend/frontend.md +++ b/.github/copilot/rules/frontend/frontend.md @@ -44,7 +44,7 @@ Guidelines for frontend TypeScript and React development, including component st - Use React Aria Components from `@repo/ui/components/ComponentName`: - Search [Components](/application/shared-webapp/ui/components) when you need to find a component - Use existing components rather than creating new ones - - Use `onPress` instead of `onClick` for event handlers + - Use `onPress` instead of `onClick` for event handlers (exception: Dialog close button uses `onClick={close}` from render prop) - Use `onAction` for menu items and list actions - Use `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -53,6 +53,9 @@ Guidelines for frontend TypeScript and React development, including component st - Colocate state with components—don't lift state unnecessarily - Use `useCallback` and `useMemo` only for proven performance issues - Throw errors sparingly and ensure error messages include a period + - Include appropriate aria labels for accessibility (e.g., `slot="title"` on Heading in dialogs) + - Disable UI during pending operations: `isDisabled={mutation.isPending}` on buttons/fields, `isDismissable={!mutation.isPending}` on modals + - Dialog sizing: `sm:w-dialog-md` (simple), `sm:w-dialog-lg` (4-6 fields), `sm:w-dialog-xl` (complex), `sm:w-dialog-2xl` (extra-large) 3. Error handling: - **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors) @@ -64,79 +67,103 @@ Guidelines for frontend TypeScript and React development, including component st - Use `isTouchDevice()` for touch vs mouse interactions - Use `isMediumViewportOrLarger()` for desktop-specific features -5. Always follow these steps when implementing changes: +5. Z-index layering for fixed-position elements (don't invent new values): + - `z-0` to `z-20`: Content layers (sticky headers, table headers) + - `z-30` to `z-40`: Navigation (top bar, mobile header) + - `z-60`: Side menu collapsed + - `z-70`: Side panes (backdrop at `z-[65]`) + - `z-80`: Side menu expanded in overlay mode (backdrop at `z-[75]`) + - `z-90`: Modal dialogs + - `z-100`: High priority modals (nested, confirmations) + - `z-[150]`: Toasts (always visible for user feedback) + - `z-[200]`: Mobile full-screen menus + - Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context + +6. Always follow these steps when implementing changes: - Consult relevant rule files and list which ones guided your implementation - Search the codebase for similar code before implementing new code - Reference existing implementations to maintain consistency -6. Build and format your changes: +7. Build and format your changes: - After each minor change, use the **execute MCP tool** with `command: "build"` for frontend - This ensures consistent code style across the codebase -7. Verify your changes: +8. Verify your changes: - When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect** - Fix any compiler warnings or test failures before proceeding ## Examples -### Example 1 - Component Structure - ```tsx -// ✅ DO: Create focused components with clear responsibilities -import { Trans } from "@lingui/react/macro"; -import { Avatar } from "@repo/ui/components/Avatar"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuHeader, MenuItem, MenuSeparator } from "@repo/ui/components/Menu"; -import { LogOutIcon, UserIcon } from "lucide-react"; - -export function AvatarMenu({ userInfo, onProfileClick, onLogoutClick }: AvatarMenuProps) { +// ✅ DO: Correct patterns +export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) { + const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen }); + const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline + + const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern + return ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ DON'T: Mix unrelated functionality in a single component -function BadAvatarMenu({ userInfo }) { // Bad: Mixing menu with logout functionality +// ❌ DON'T: Common anti-patterns +function BadUserDialog({ users, selectedId, isOpen, onClose }) { + const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values + const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated + + useEffect(() => { // ❌ useEffect for calculations - compute inline instead + setFilteredUsers(users.filter(u => u.isActive)); + setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types + }, [users, selectedId]); + + const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need + return `${user.firstName} ${user.lastName}`; + }, []); + + const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log + return ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.github/copilot/rules/frontend/modal-dialog.md b/.github/copilot/rules/frontend/modal-dialog.md deleted file mode 100644 index f82c05e414..0000000000 --- a/.github/copilot/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,83 +0,0 @@ -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/.windsurf/rules/frontend/frontend.md b/.windsurf/rules/frontend/frontend.md index 6316ca51f1..cab5791023 100644 --- a/.windsurf/rules/frontend/frontend.md +++ b/.windsurf/rules/frontend/frontend.md @@ -50,7 +50,7 @@ Guidelines for frontend TypeScript and React development, including component st - Use React Aria Components from `@repo/ui/components/ComponentName`: - Search [Components](/application/shared-webapp/ui/components) when you need to find a component - Use existing components rather than creating new ones - - Use `onPress` instead of `onClick` for event handlers + - Use `onPress` instead of `onClick` for event handlers (exception: Dialog close button uses `onClick={close}` from render prop) - Use `onAction` for menu items and list actions - Use `...` for JSX translations, `t` macro for strings - Use TanStack Query for API interactions via `api.useQuery()` and `api.useMutation()` @@ -59,6 +59,9 @@ Guidelines for frontend TypeScript and React development, including component st - Colocate state with components—don't lift state unnecessarily - Use `useCallback` and `useMemo` only for proven performance issues - Throw errors sparingly and ensure error messages include a period + - Include appropriate aria labels for accessibility (e.g., `slot="title"` on Heading in dialogs) + - Disable UI during pending operations: `isDisabled={mutation.isPending}` on buttons/fields, `isDismissable={!mutation.isPending}` on modals + - Dialog sizing: `sm:w-dialog-md` (simple), `sm:w-dialog-lg` (4-6 fields), `sm:w-dialog-xl` (complex), `sm:w-dialog-2xl` (extra-large) 3. Error handling: - **Errors are handled globally**—`shared-webapp/infrastructure/http/errorHandler.ts` automatically shows toast notifications with the server's error message (don't manually show toasts for errors) @@ -70,79 +73,103 @@ Guidelines for frontend TypeScript and React development, including component st - Use `isTouchDevice()` for touch vs mouse interactions - Use `isMediumViewportOrLarger()` for desktop-specific features -5. Always follow these steps when implementing changes: +5. Z-index layering for fixed-position elements (don't invent new values): + - `z-0` to `z-20`: Content layers (sticky headers, table headers) + - `z-30` to `z-40`: Navigation (top bar, mobile header) + - `z-60`: Side menu collapsed + - `z-70`: Side panes (backdrop at `z-[65]`) + - `z-80`: Side menu expanded in overlay mode (backdrop at `z-[75]`) + - `z-90`: Modal dialogs + - `z-100`: High priority modals (nested, confirmations) + - `z-[150]`: Toasts (always visible for user feedback) + - `z-[200]`: Mobile full-screen menus + - Note: Dropdowns, tooltips, and popovers use React Aria's overlay system which manages stacking relative to their context + +6. Always follow these steps when implementing changes: - Consult relevant rule files and list which ones guided your implementation - Search the codebase for similar code before implementing new code - Reference existing implementations to maintain consistency -6. Build and format your changes: +7. Build and format your changes: - After each minor change, use the **execute MCP tool** with `command: "build"` for frontend - This ensures consistent code style across the codebase -7. Verify your changes: +8. Verify your changes: - When a feature is complete, run these MCP tools for frontend in sequence: **build**, **format**, **inspect** - Fix any compiler warnings or test failures before proceeding ## Examples -### Example 1 - Component Structure - ```tsx -// ✅ DO: Create focused components with clear responsibilities -import { Trans } from "@lingui/react/macro"; -import { Avatar } from "@repo/ui/components/Avatar"; -import { Button } from "@repo/ui/components/Button"; -import { Menu, MenuHeader, MenuItem, MenuSeparator } from "@repo/ui/components/Menu"; -import { LogOutIcon, UserIcon } from "lucide-react"; - -export function AvatarMenu({ userInfo, onProfileClick, onLogoutClick }: AvatarMenuProps) { +// ✅ DO: Correct patterns +export function UserPicker({ isOpen, isPending, onOpenChange }: UserPickerProps) { + const { data } = api.useQuery("get", "/api/account-management/users", { enabled: isOpen }); + const activeUsers = (data?.users ?? []).filter((u) => u.isActive); // ✅ Compute derived values inline + + const handleChangeSelection = (keys: Selection) => { /* ... */ }; // ✅ handleVerbNoun pattern + return ( - - -
- -
-

{userInfo.fullName}

-

{userInfo.title ?? userInfo.email}

-
-
-
- - - Profile - - - - - Log out - -
+ // ✅ Prevent dismiss during pending + // ✅ Use dialog width classes (not max-w-lg) + {({ close }) => ( // ✅ Dialog render prop provides close function + <> + // ✅ Close button pattern (onClick is exception) + + Select users + + + + {activeUsers.map((user) => ( + + {`${user.firstName} ${user.lastName}`} + + ))} + + + + + + + )} + + ); } -// ❌ DON'T: Mix unrelated functionality in a single component -function BadAvatarMenu({ userInfo }) { // Bad: Mixing menu with logout functionality +// ❌ DON'T: Common anti-patterns +function BadUserDialog({ users, selectedId, isOpen, onClose }) { + const [filteredUsers, setFilteredUsers] = useState([]); // ❌ State for derived values + const [isAdmin, setIsAdmin] = useState(false); // ❌ Duplicate state that can be calculated + + useEffect(() => { // ❌ useEffect for calculations - compute inline instead + setFilteredUsers(users.filter(u => u.isActive)); + setIsAdmin(users.some(u => u.id === selectedId && u.role === "admin")); // ❌ Hardcode strings - use API contract types + }, [users, selectedId]); + + const getDisplayName = useCallback((user) => { // ❌ Premature useCallback without performance need + return `${user.firstName} ${user.lastName}`; + }, []); + + const handleSelect = (id) => console.log(id); // ❌ "handle" + noun (use handleSelectUser), console.log + return ( -
// ❌ DON'T: Use CSS styles instead of Tailwind -
-
// ❌ Unnecessary nested
-
- User avatar // ❌ DON'T: Use native -

{userInfo.fullName}

-
-
-
- - -
+ // ❌ Missing isDismissable={!isPending} + // ❌ max-w-lg (use w-dialog-md), hardcoded colors (use bg-background) +

User Mgmt

// ❌ Native

(use Heading), acronym "Mgmt", missing +
    // ❌ Native
      - use ListBox + {filteredUsers.map(user => ( +
    • handleSelect(user.id)}> // ❌ Native
    • , onClick (use onAction) + // ❌ Native - use Avatar + {user.email} // ❌ text-sm with Text causes blur + {getDisplayName(user)} +
    • + ))} +
    + // ❌ Missing isDisabled/isPending, missing +

+
); } ``` diff --git a/.windsurf/rules/frontend/modal-dialog.md b/.windsurf/rules/frontend/modal-dialog.md deleted file mode 100644 index fdd1aceeb1..0000000000 --- a/.windsurf/rules/frontend/modal-dialog.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -trigger: glob -globs: *Dialog.tsx,*Modal.tsx -description: Rules for modal dialogs using React Aria Components ---- - -# Modal Dialog - -Guidelines for implementing modal dialogs in the frontend, focusing on accessibility, component usage, and translation patterns. - -## Implementation - -1. Use React Aria Components from `@repo/ui/components` -2. Use the `Dialog` component from shared-webapp for all dialogs and modal dialogs -3. Manage dialog state with React hooks (`useState` or context depending on scope) -4. Use `onPress` instead of `onClick` for event handlers -5. Include appropriate aria labels for accessibility -6. Use `...` or t\`...\` for translations (content should be plain English) -7. Apply a dialog width class (`sm:w-dialog-md`, `sm:w-dialog-lg`, `sm:w-dialog-xl`, or `sm:w-dialog-2xl`) to every `Dialog` component -8. Use `isDismissable={!mutation.isPending}` to prevent closing during save operations - -## Dialog Sizing - -Every dialog must have a fixed width class to ensure consistent sizing across languages: - -- `sm:w-dialog-md` — most dialogs: alerts, confirmations, simple forms (1-3 fields) -- `sm:w-dialog-lg` — standard forms with multiple fields (4-6 fields) -- `sm:w-dialog-xl` — complex forms with many fields, file uploads, or multi-section content -- `sm:w-dialog-2xl` — extra-large dialogs with dual-list interfaces or very complex layouts - -The `AlertDialog` component defaults to `sm:w-dialog-md` but can be overridden. - -## Examples - -### Example 1 - Simple Form Dialog - -```typescript -const [isOpen, setIsOpen] = useState(false); - - - - setIsOpen(false)} - className="absolute top-2 right-2 h-10 w-10 cursor-pointer p-2 hover:bg-muted" - /> - Enter the email address.}> - - Invite user - - - -
- - - - - - - -
-
-
-``` - -### Example 2 - Alert Dialog - -```typescript - - - Are you sure you want to delete this user? - - -``` - diff --git a/application/account-management/WebApp/federated-modules/common/AuthSyncModal.tsx b/application/account-management/WebApp/federated-modules/common/AuthSyncModal.tsx index b773d3aaea..6c58bd9906 100644 --- a/application/account-management/WebApp/federated-modules/common/AuthSyncModal.tsx +++ b/application/account-management/WebApp/federated-modules/common/AuthSyncModal.tsx @@ -86,7 +86,7 @@ export default function AuthSyncModal({ isOpen, type, newTenantName, onPrimaryAc return ( - + {() => ( <> diff --git a/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx b/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx index 88bf9d5aef..91ab22341d 100644 --- a/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx +++ b/application/account-management/WebApp/federated-modules/common/SwitchingAccountLoader.tsx @@ -3,7 +3,7 @@ import { Loader2 } from "lucide-react"; export function SwitchingAccountLoader() { return ( -
+

diff --git a/application/account-management/WebApp/routes/(index)/index.tsx b/application/account-management/WebApp/routes/(index)/index.tsx index a17529ed3a..2dbb975b8f 100644 --- a/application/account-management/WebApp/routes/(index)/index.tsx +++ b/application/account-management/WebApp/routes/(index)/index.tsx @@ -1,4 +1,7 @@ import { Trans } from "@lingui/react/macro"; +import { loginPath, signUpPath } from "@repo/infrastructure/auth/constants"; +import { useIsAuthenticated } from "@repo/infrastructure/auth/hooks"; +import { Link } from "@repo/ui/components/Link"; import { createFileRoute } from "@tanstack/react-router"; import { PublicFooter } from "@/shared/components/PublicFooter"; import { PublicNavigation } from "@/shared/components/PublicNavigation"; @@ -6,6 +9,8 @@ import { PublicNavigation } from "@/shared/components/PublicNavigation"; export const Route = createFileRoute("/(index)/")({ beforeLoad: () => ({ disableAuthSync: true }), component: function LandingPage() { + const isAuthenticated = useIsAuthenticated(); + return (

{/* Hero Section */} @@ -27,6 +32,39 @@ export const Route = createFileRoute("/(index)/")({ Replace this sample page with your own product information and branding.

+ + {/* CTAs */} +
+ {isAuthenticated ? ( + + Go to app + + ) : ( + <> + + Get started + + + Log in + + + )} +
diff --git a/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx b/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx index a416d3c272..c212327216 100644 --- a/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx +++ b/application/account-management/WebApp/routes/admin/account/-components/DeleteAccountConfirmation.tsx @@ -14,7 +14,7 @@ type DeleteAccountConfirmationProps = { export default function DeleteAccountConfirmation({ isOpen, onOpenChange }: Readonly) { return ( - + {({ close }) => ( <> diff --git a/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx b/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx index 216455116c..7b6046bb13 100644 --- a/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx +++ b/application/account-management/WebApp/routes/admin/users/-components/ChangeUserRoleDialog.tsx @@ -1,14 +1,20 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; -import { AlertDialog } from "@repo/ui/components/AlertDialog"; +import { Avatar } from "@repo/ui/components/Avatar"; import { Button } from "@repo/ui/components/Button"; -import { DialogContent, DialogFooter } from "@repo/ui/components/DialogFooter"; +import { Dialog } from "@repo/ui/components/Dialog"; +import { DialogContent, DialogFooter, DialogHeader } from "@repo/ui/components/DialogFooter"; +import { Form } from "@repo/ui/components/Form"; +import { Heading } from "@repo/ui/components/Heading"; import { Modal } from "@repo/ui/components/Modal"; -import { Select, SelectItem } from "@repo/ui/components/Select"; +import { Radio, RadioGroup } from "@repo/ui/components/RadioGroup"; +import { Text } from "@repo/ui/components/Text"; import { toastQueue } from "@repo/ui/components/Toast"; -import { useCallback, useState } from "react"; +import { getInitials } from "@repo/utils/string/getInitials"; +import { useQueryClient } from "@tanstack/react-query"; +import { XIcon } from "lucide-react"; +import { useState } from "react"; import { api, type components, UserRole } from "@/shared/lib/api/client"; -import { getUserRoleLabel } from "@/shared/lib/api/userRole"; type UserDetails = components["schemas"]["UserDetails"]; @@ -19,19 +25,14 @@ interface ChangeUserRoleDialogProps { } export function ChangeUserRoleDialog({ user, isOpen, onOpenChange }: Readonly) { + const queryClient = useQueryClient(); const [selectedRole, setSelectedRole] = useState(null); - const changeUserRoleMutation = api.useMutation("put", "/api/account-management/users/{id}/change-user-role"); - const handleConfirm = useCallback(async () => { - if (!user || !selectedRole) { - return; - } - - try { - await changeUserRoleMutation.mutateAsync({ - params: { path: { id: user.id } }, - body: { userRole: selectedRole } - }); + const changeUserRoleMutation = api.useMutation("put", "/api/account-management/users/{id}/change-user-role", { + onSuccess: () => { + if (!user) { + return; + } const userDisplayName = `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email; toastQueue.add({ @@ -39,58 +40,131 @@ export function ChangeUserRoleDialog({ user, isOpen, onOpenChange }: Readonly { - onOpenChange(false); - setSelectedRole(null); - }, [onOpenChange]); + if (!user) { + return null; + } + + const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.email; + const currentRole = selectedRole ?? user.role; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + changeUserRoleMutation.mutate({ + params: { + path: { id: user.id } + }, + body: { userRole: currentRole } + }); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) { + setSelectedRole(null); + } + onOpenChange(open); + }; return ( - - -
- -

- - Select a new role for{" "} - {user ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim() || user.email : ""} - -

+ + + {({ close }) => ( + <> + + + + Change user role + + -
- -
-
+
+ +
+ +
+ {displayName} + {user.title && {user.title}} + {user.email} +
+
- - - - -
-
+ setSelectedRole(value as UserRole)} + orientation="vertical" + > +
+ +
+ + Owner + + + Full access including user roles and account settings + +
+
+
+
+ +
+ + Admin + + + Full access except changing user roles and account settings + +
+
+
+
+ +
+ + Member + + + Standard user access + +
+
+
+
+ + + + + + + + )} +
); } diff --git a/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx b/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx index bb1830128a..79f6e5c5bf 100644 --- a/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx +++ b/application/account-management/WebApp/routes/admin/users/-components/UserProfileSidePane.tsx @@ -246,12 +246,12 @@ export function UserProfileSidePane({ return ( <> {/* Backdrop for small screens */} - {isSmallScreen &&