Files
slp-modular-cms/aidlc-docs/features/cms-frontend/construction/unit-5/functional-design/frontend-components.md
T
2026-06-22 21:14:22 +02:00

9.1 KiB
Raw Blame History

Frontend Components — Unit 5: User Management

Component Hierarchy

graph TD
    role_guard["RoleGuard\n(Owner or Admin only)"]
    users_page["UsersPage\n(pages/UsersPage.tsx)"]
    page_header["Page Header\nh1 + Invite button"]
    user_table["UserTable\n(shadcn Table)"]
    table_row["UserTableRow\n(×n per user)"]
    row_actions["RowActionsMenu\n(shadcn DropdownMenu)"]
    copy_link["CopyInviteLinkAction\n(if invitationPending)"]
    change_role["ChangeRoleAction\n(role-filtered options)"]
    invite_dialog["InviteUserDialog\n(shadcn Dialog)"]
    step1["Step1InviteForm\n(email + role select)"]
    step2["Step2ShareLinkView\n(link + copy button)"]

    role_guard --> users_page
    users_page --> page_header
    users_page --> user_table
    users_page --> invite_dialog
    page_header --> invite_dialog
    user_table --> table_row
    table_row --> row_actions
    row_actions --> copy_link
    row_actions --> change_role
    invite_dialog --> step1
    invite_dialog --> step2

    classDef guard_node fill:#FF9800,stroke:#e65100,stroke-width:1px,color:#000
    classDef page_node fill:#2196F3,stroke:#0d47a1,stroke-width:1px,color:#000
    classDef table_node fill:#63b3ed,stroke:#2b6cb0,stroke-width:1px,color:#000
    classDef action_node fill:#9C27B0,stroke:#4a148c,stroke-width:1px,color:#000
    classDef dialog_node fill:#4CAF50,stroke:#2e7d32,stroke-width:1px,color:#000

    class role_guard guard_node
    class users_page,page_header page_node
    class user_table,table_row table_node
    class row_actions,copy_link,change_role action_node
    class invite_dialog,step1,step2 dialog_node

Text alternative: RoleGuard wraps UsersPage; the page contains a header (with Invite button), a UserTable (rows with DropdownMenu actions), and an InviteUserDialog (Step 1 form → Step 2 share link).


Component Specifications

UsersPage — pages/UsersPage.tsx

Purpose: Main user management page. Fetches user list, renders table and invite dialog.

State:

  • dialogOpen: boolean — controls InviteUserDialog visibility
  • dialogStep: 1 | 2 — which dialog step is shown
  • generatedLink: string | null — invite link returned after successful invite

API integration:

  • useUsers() — fetches user list on mount; staleTime: 30s

Render structure:

  • RoleGuard enforced at router level (router.tsx)
  • Page title (t('nav.users'))
  • "Invite User" button (opens dialog)
  • UserTable with fetched data
  • InviteUserDialog (controlled by state above)

UserTable (inline in UsersPage or extracted component)

Purpose: Renders the shadcn Table with one row per UserListItem.

Columns:

Column Field Notes
Name user.name Plain text
Email user.email Plain text
Role user.role Colored badge (Owner=red, Admin=orange, User=blue)
Status user.isActive + user.invitationPending "Active", "Inactive", or "Pending invite" badge
Created user.createdAt Formatted as locale date string
Actions DropdownMenu (see RowActionsMenu)

Props:

interface UserTableProps {
    users: UserListItem[];
    currentUser: User;             // from AuthContext — for role-based action filtering
    onRoleChange: (userId: string, newRole: UserRole) => void;
}

RowActionsMenu — DropdownMenu per row

Purpose: Contextual actions per user row.

Actions:

  1. Copy invite link — visible only when user.invitationPending === true; calls navigator.clipboard.writeText(user.inviteLink!) and shows a toast
  2. Change role — opens an inline role picker (sub-menu or inline select); available roles depend on requester:
    • Requester is Owner: all three roles shown (Owner, Administrator, User)
    • Requester is Administrator: only User → Administrator promotion (target must be User role)

Props:

interface RowActionsMenuProps {
    user: UserListItem;
    currentUser: User;
    onRoleChange: (userId: string, newRole: UserRole) => void;
}

InviteUserDialog — components/users/InviteUserDialog.tsx

Purpose: Two-step modal dialog for inviting a new user.

Props:

interface InviteUserDialogProps {
    open: boolean;
    onOpenChange: (open: boolean) => void;
}

Step 1 — InviteUserForm:

  • Fields: email (text input), role (Select: Administrator | User)
  • Validation: zod schema (see business-rules.md)
  • On submit: calls useInviteUser() mutation
  • On success: transitions to Step 2 with the returned inviteLink
  • On error: shows FormErrorBanner with backend error message

Step 2 — Step2ShareLinkView:

  • Read-only text input displaying the full invite link
  • "Copy to clipboard" button (copies link, shows toast)
  • "Close" button (closes dialog, triggers useUsers query invalidation)

State (internal):

const [step, setStep] = useState<1 | 2>(1);
const [inviteLink, setInviteLink] = useState<string | null>(null);

// Reset to step 1 on open
useEffect(() => { if (!open) { setStep(1); setInviteLink(null); } }, [open]);

Hooks

useUsers — api/useUsers.ts

// GET /api/v1/Users
export function useUsers(): UseQueryResult<UserListItem[]>
  • Query key: ['users']
  • staleTime: 30 000 ms (30s)
  • Invalidated by: useInviteUser.onSuccess, useChangeRole.onSuccess

useInviteUser — api/useUsers.ts

// POST /api/v1/Users/invite
export function useInviteUser(): UseMutationResult<InviteUserResponse, Error, InviteUserRequest>
  • onSuccess: invalidates ['users'] query

useChangeRole — api/useUsers.ts

// PUT /api/v1/Users/{userId}/role
export function useChangeRole(): UseMutationResult<void, Error, { userId: string; newRole: UserRole }>
  • onSuccess: invalidates ['users'] query

useValidateInvitation (migrated) — api/useInvitation.ts

// GET /api/v1/Invitation/validate?token=...
export function useValidateInvitation(token: string | null): UseQueryResult<InvitationValidation>
  • Migrated from useState/useEffect to useQuery
  • Uses InvitationValidation from types.ts (replacing local interface)
  • enabled: !!token (skip query when token is null)

useCompleteInvitation (migrated) — api/useInvitation.ts

// POST /api/v1/Invitation/complete
export function useCompleteInvitation(): UseMutationResult<void, Error, InviteCompleteRequest>
  • Migrated from useState/useCallback to useMutation
  • Uses InviteCompleteRequest from types.ts

Form Validation Rules

InviteUserForm (zod)

Field Rule Error message
email z.string().email() "Valid email address required"
role z.enum(['Administrator', 'User']) "Role selection is required"

API Integration Summary

Component / Hook Endpoint Method Auth required
useUsers /api/v1/Users GET Yes (Owner / Admin)
useInviteUser /api/v1/Users/invite POST Yes (Owner / Admin)
useChangeRole /api/v1/Users/{userId}/role PUT Yes (Owner / Admin)
useValidateInvitation /api/v1/Invitation/validate GET No
useCompleteInvitation /api/v1/Invitation/complete POST No

i18n Keys (en + nl)

{
  "users": {
    "title": "Users",
    "inviteButton": "Invite User",
    "table": {
      "name": "Name",
      "email": "Email",
      "role": "Role",
      "status": "Status",
      "createdAt": "Created",
      "actions": "Actions"
    },
    "status": {
      "active": "Active",
      "inactive": "Inactive",
      "pendingInvite": "Pending invite"
    },
    "actions": {
      "copyInviteLink": "Copy invite link",
      "changeRole": "Change role",
      "linkCopied": "Invite link copied to clipboard"
    },
    "invite": {
      "title": "Invite User",
      "emailLabel": "Email address",
      "roleLabel": "Role",
      "submitButton": "Send invitation",
      "step2Title": "Invitation created",
      "step2Description": "Share the link below with the invitee.",
      "copyLink": "Copy link",
      "close": "Close"
    },
    "roles": {
      "Owner": "Owner",
      "Administrator": "Administrator",
      "User": "User"
    }
  }
}

MSW Mocks (Unit 5 additions)

Extend frontend/src/mocks/users/handlers.ts:

Handler Method Path Response
List users GET /api/v1/Users Array of UserListItem (including 1 pending)
Invite user POST /api/v1/Users/invite { inviteLink: '/invite/complete?token=mock-token' }
Change role PUT /api/v1/Users/:userId/role 200 OK (no body)

Unit Test Scope

Test file Coverage
api/useUsers.test.ts useUsers (loading, success, error); useInviteUser (success, API error); useChangeRole (success, 403)
pages/UsersPage.test.tsx Renders table with user list; Invite button opens dialog; Step 1 → Step 2 transition; copy invite link toast
components/users/InviteUserDialog.test.tsx Step 1 form validation; successful invite shows Step 2; error shows FormErrorBanner
api/useInvitation.test.ts useValidateInvitation (valid token, invalid token); useCompleteInvitation (success, error) — replaces Unit 2 tests