user-management-component-redesign.md 36 KB

User Management Component Redesign

Executive Summary

This document outlines the redesign of the Spry Authentication user management components. The redesign focuses on:

  1. Converting UserManagementPage to UserManagementComponent for better placement control
  2. Removing all CSS classes to avoid conflicts with application-defined styles
  3. Implementing an HTML5 <details>/<summary> pattern for user display
  4. Using separate view/edit components that swap via HTMX for cleaner separation
  5. Integrating permission editing directly into the edit component (removing standalone PermissionEditorComponent)

Current Implementation Overview

Component Hierarchy

graph TD
    A[UserManagementPage - PageComponent] --> B[UserListComponent]
    A --> C[UserFormComponent]
    B --> D[UserListItemComponent - per user]
    C --> E[PermissionEditorComponent]

Current Components

Component Type Purpose
UserManagementPage PageComponent Full HTML page with embedded CSS, permission check, orchestrates child components
UserListComponent Component Table of users with search/pagination, creates UserListItemComponent per row
UserListItemComponent Component Single table row with user info and action buttons
UserFormComponent Component Modal form for create/edit with validation
PermissionEditorComponent Component Checkbox-based permission selection

Current Issues

  1. Tight Coupling to Page Structure: UserManagementPage is a PageComponent that generates a full HTML document, limiting where it can be placed
  2. CSS Class Pollution: Extensive use of CSS classes like .admin-container, .btn, .modal-overlay, .spry-user-list may conflict with application styles
  3. Modal-Based Editing: Separate modal form disrupts the flow and requires additional clicks
  4. Complex Component Tree: Five separate components increase maintenance burden

New Design

Component Hierarchy

graph TD
    A[UserManagementComponent - Container] --> B[UserDetailsComponent - view mode per user]
    A --> C[UserDetailEditComponent - edit mode swaps in]
    A --> D[NewUserComponent - for creation]
    C --> E[Integrated Permission Editing]
    D --> E

Simplified Component Structure

Component Type Purpose
UserManagementComponent Component Container with header, new user button, user list outlet
UserDetailsComponent Component View-only <details> element for displaying one user
UserDetailEditComponent Component Edit mode <details> element with form fields and integrated permission editing
NewUserComponent Component Create form using same pattern as edit, with integrated permission editing

Key Changes

  1. UserManagementPageUserManagementComponent:

    • Changed from PageComponent to Component
    • No longer generates full HTML document
    • Application can place it anywhere
  2. Separate View/Edit Components:

    • UserDetailsComponent handles read-only display
    • UserDetailEditComponent handles editing (swapped in via HTMX)
    • Clean separation of concerns - each component has a single responsibility
  3. Remove PermissionEditorComponent:

    • Permission editing functionality integrated directly into UserDetailEditComponent and NewUserComponent
    • Reduces component complexity and eliminates unnecessary abstraction
  4. Remove UserFormComponent, UserListComponent, UserListItemComponent:

    • Functionality distributed among new focused components

HTML Structure Design

UserManagementComponent

<div sid="user-management" id="user-management" hx-swap="outerHTML">
    <script spry-res="htmx.js"></script>
    
    <!-- Header with Create Button -->
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
        <h3 style="margin: 0;">Users</h3>
        <button sid="create-btn" 
                spry-action=":ShowCreateUser"
                spry-target="user-management"
                style="padding: 0.5rem 1rem; cursor: pointer;">
            + Create User
        </button>
    </div>
    
    <!-- Success/Error Messages -->
    <div spry-if="this.success_message != null" 
         style="padding: 0.75rem; margin-bottom: 1rem; background: #d4edda; color: #155724; border-radius: 4px;">
        <span content-expr="this.success_message"></span>
    </div>
    
    <div spry-if="this.error_message != null"
         style="padding: 0.75rem; margin-bottom: 1rem; background: #f8d7da; color: #721c24; border-radius: 4px;">
        <span content-expr="this.error_message"></span>
    </div>
    
    <!-- New User Form (conditionally visible) -->
    <div spry-if="this.show_create_form" sid="new-user-container">
        <spry-component name="Spry.Authentication.Components.NewUserComponent" sid="new-user"/>
    </div>
    
    <!-- User List -->
    <div sid="user-list" style="display: flex; flex-direction: column; gap: 0.5rem;">
        <spry-outlet sid="users"/>
    </div>
</div>

UserDetailsComponent - View Mode Only

This component handles read-only display of a single user. When the user clicks "Edit", the entire component is swapped out for UserDetailEditComponent.

<details sid="user-details"
         id-expr="'user-' + this.user_id"
         hx-swap="outerHTML"
         open>
    
    <!-- Summary: Always visible, shows key info -->
    <summary style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; background: #f8f9fa; border-radius: 4px; cursor: pointer;">
        <span content-expr="this.username" style="font-weight: 500; min-width: 120px;"></span>
        <span content-expr="this.email" style="color: #6c757d;"></span>
        <span spry-if="this.permissions.length > 0" style="margin-left: auto; font-size: 0.85rem; color: #495057;">
            <span content-expr="this.permissions.length"></span> permission(s)
        </span>
    </summary>
    
    <!-- Details: Visible when expanded - VIEW ONLY -->
    <div style="padding: 1rem; border: 1px solid #e9ecef; border-top: none; border-radius: 0 0 4px 4px;">
        <table style="width: 100%; border-collapse: collapse;">
            <tbody>
                <tr>
                    <td style="padding: 0.5rem 0; font-weight: 500; width: 150px;">User ID</td>
                    <td style="padding: 0.5rem 0;"><code content-expr="this.user_id"></code></td>
                </tr>
                <tr>
                    <td style="padding: 0.5rem 0; font-weight: 500;">Username</td>
                    <td style="padding: 0.5rem 0;"><span content-expr="this.username"></span></td>
                </tr>
                <tr>
                    <td style="padding: 0.5rem 0; font-weight: 500;">Email</td>
                    <td style="padding: 0.5rem 0;"><span content-expr="this.email"></span></td>
                </tr>
                <tr>
                    <td style="padding: 0.5rem 0; font-weight: 500;">Created</td>
                    <td style="padding: 0.5rem 0;"><span content-expr="this.created_at"></span></td>
                </tr>
                <tr>
                    <td style="padding: 0.5rem 0; font-weight: 500; vertical-align: top;">Permissions</td>
                    <td style="padding: 0.5rem 0;">
                        <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
                            <spry-outlet sid="permission-badges"/>
                        </div>
                    </td>
                </tr>
            </tbody>
        </table>
        
        <!-- View Mode Actions -->
        <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
            <button sid="edit-btn"
                    spry-action=":StartEdit"
                    spry-target="user-details"
                    style="padding: 0.25rem 0.75rem; cursor: pointer; font-size: 0.875rem;">
                Edit
            </button>
            <button sid="delete-btn"
                    spry-action=":DeleteUser"
                    spry-target="user-management"
                    style="padding: 0.25rem 0.75rem; cursor: pointer; font-size: 0.875rem; background: #dc3545; color: white; border: none; border-radius: 4px;">
                Delete
            </button>
        </div>
    </div>
</details>

UserDetailEditComponent - Edit Mode with Integrated Permissions

This component is swapped in when editing. It includes inline permission editing (no separate component).

<details sid="user-details-edit"
         id-expr="'user-' + this.user_id"
         hx-swap="outerHTML"
         open>
    
    <!-- Summary: Shows user being edited -->
    <summary style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; background: #fff3cd; border-radius: 4px; cursor: pointer; border: 2px solid #ffc107;">
        <span content-expr="this.username" style="font-weight: 500; min-width: 120px;"></span>
        <span style="color: #856404; font-style: italic;">Editing...</span>
    </summary>
    
    <!-- Edit Form -->
    <div style="padding: 1rem; border: 2px solid #ffc107; border-top: none; border-radius: 0 0 4px 4px; background: #fffdf5;">
        <form sid="edit-form"
              spry-action=":SaveEdit"
              spry-target="user-details-edit">
            <input type="hidden" name="user_id" sid="user-id-input" value-expr="this.user_id"/>
            
            <table style="width: 100%; border-collapse: collapse;">
                <tbody>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500; width: 150px;">User ID</td>
                        <td style="padding: 0.5rem 0;"><code content-expr="this.user_id"></code></td>
                    </tr>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500;">Username *</td>
                        <td style="padding: 0.5rem 0;">
                            <input type="text"
                                   name="username"
                                   sid="username-input"
                                   required
                                   minlength="3"
                                   pattern="[a-zA-Z0-9_]+"
                                   style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"
                                   value-expr="this.username"/>
                        </td>
                    </tr>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500;">Email *</td>
                        <td style="padding: 0.5rem 0;">
                            <input type="email"
                                   name="email"
                                   sid="email-input"
                                   required
                                   style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"
                                   value-expr="this.email"/>
                        </td>
                    </tr>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500;">New Password</td>
                        <td style="padding: 0.5rem 0;">
                            <input type="password"
                                   name="new_password"
                                   sid="password-input"
                                   minlength="8"
                                   placeholder="Leave blank to keep current"
                                   style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"/>
                            <small style="color: #6c757d;">Minimum 8 characters if changing</small>
                        </td>
                    </tr>
                    
                    <!-- Integrated Permission Editing -->
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500; vertical-align: top;">Permissions</td>
                        <td style="padding: 0.5rem 0;">
                            <!-- Common Permissions as Checkboxes -->
                            <div style="margin-bottom: 0.75rem;">
                                <div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Common:</div>
                                <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-management" value="user-management"/>
                                        User Management
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-create" value="user.create"/>
                                        Create Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-read" value="user.read"/>
                                        Read Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-update" value="user.update"/>
                                        Update Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-delete" value="user.delete"/>
                                        Delete Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_admin" value="admin"/>
                                        Admin
                                    </label>
                                </div>
                            </div>
                            
                            <!-- Custom Permissions -->
                            <div>
                                <div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Custom Permissions:</div>
                                <div sid="custom-perms" style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem;">
                                    <spry-outlet sid="custom-permission-tags"/>
                                </div>
                                <div style="display: flex; gap: 0.25rem;">
                                    <input type="text"
                                           name="new_permission"
                                           sid="new-perm-input"
                                           placeholder="e.g., reports.view"
                                           style="flex: 1; padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 0.875rem;"/>
                                    <button type="button"
                                            sid="add-perm-btn"
                                            spry-action=":AddPermission"
                                            spry-target="user-details-edit"
                                            style="padding: 0.25rem 0.5rem; font-size: 0.875rem; cursor: pointer;">
                                        Add
                                    </button>
                                </div>
                            </div>
                        </td>
                    </tr>
                </tbody>
            </table>
            
            <!-- Error Message -->
            <div spry-if="this.error_message != null"
                 style="padding: 0.5rem; margin: 0.5rem 0; background: #f8d7da; color: #721c24; border-radius: 4px;">
                <span content-expr="this.error_message"></span>
            </div>
            
            <!-- Edit Actions -->
            <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
                <button type="submit" style="padding: 0.5rem 1rem; cursor: pointer;">Save Changes</button>
                <button type="button"
                        sid="cancel-btn"
                        spry-action=":CancelEdit"
                        spry-target="user-details-edit"
                        style="padding: 0.5rem 1rem; cursor: pointer; background: #6c757d; color: white; border: none; border-radius: 4px;">
                    Cancel
                </button>
            </div>
        </form>
    </div>
</details>

NewUserComponent - For Creating New Users

Uses the same pattern as edit mode with integrated permission editing.

<details sid="new-user"
         id="new-user"
         hx-swap="outerHTML"
         open>
    
    <summary style="display: flex; align-items: center; gap: 1rem; padding: 0.75rem; background: #e7f3ff; border-radius: 4px; cursor: pointer; border: 2px dashed #007bff;">
        <span style="font-weight: 500; color: #007bff;">+ New User</span>
    </summary>
    
    <div style="padding: 1rem; border: 2px dashed #007bff; border-top: none; border-radius: 0 0 4px 4px; background: #f8faff;">
        <form sid="create-form"
              spry-action=":CreateUser"
              spry-target="new-user">
            
            <table style="width: 100%; border-collapse: collapse;">
                <tbody>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500; width: 150px;">Username *</td>
                        <td style="padding: 0.5rem 0;">
                            <input type="text"
                                   name="username"
                                   sid="username-input"
                                   required
                                   minlength="3"
                                   pattern="[a-zA-Z0-9_]+"
                                   placeholder="Enter username"
                                   style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"/>
                            <small style="color: #6c757d;">Alphanumeric and underscores only, min 3 chars</small>
                        </td>
                    </tr>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500;">Email *</td>
                        <td style="padding: 0.5rem 0;">
                            <input type="email"
                                   name="email"
                                   sid="email-input"
                                   required
                                   placeholder="Enter email address"
                                   style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"/>
                        </td>
                    </tr>
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500;">Password *</td>
                        <td style="padding: 0.5rem 0;">
                            <input type="password"
                                   name="password"
                                   sid="password-input"
                                   required
                                   minlength="8"
                                   placeholder="Enter password"
                                   style="width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px;"/>
                            <small style="color: #6c757d;">Minimum 8 characters</small>
                        </td>
                    </tr>
                    
                    <!-- Integrated Permission Editing (same as edit component) -->
                    <tr>
                        <td style="padding: 0.5rem 0; font-weight: 500; vertical-align: top;">Permissions</td>
                        <td style="padding: 0.5rem 0;">
                            <div style="margin-bottom: 0.75rem;">
                                <div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Common:</div>
                                <div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-management" value="user-management"/>
                                        User Management
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-create" value="user.create"/>
                                        Create Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-read" value="user.read"/>
                                        Read Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-update" value="user.update"/>
                                        Update Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_user-delete" value="user.delete"/>
                                        Delete Users
                                    </label>
                                    <label style="display: flex; align-items: center; gap: 0.25rem; font-size: 0.875rem;">
                                        <input type="checkbox" name="perm_admin" value="admin"/>
                                        Admin
                                    </label>
                                </div>
                            </div>
                            
                            <div>
                                <div style="font-size: 0.85rem; color: #6c757d; margin-bottom: 0.25rem;">Custom Permissions:</div>
                                <div sid="custom-perms" style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.5rem;">
                                    <spry-outlet sid="custom-permission-tags"/>
                                </div>
                                <div style="display: flex; gap: 0.25rem;">
                                    <input type="text"
                                           name="new_permission"
                                           sid="new-perm-input"
                                           placeholder="e.g., reports.view"
                                           style="flex: 1; padding: 0.25rem 0.5rem; border: 1px solid #ced4da; border-radius: 4px; font-size: 0.875rem;"/>
                                    <button type="button"
                                            sid="add-perm-btn"
                                            spry-action=":AddPermission"
                                            spry-target="new-user"
                                            style="padding: 0.25rem 0.5rem; font-size: 0.875rem; cursor: pointer;">
                                        Add
                                    </button>
                                </div>
                            </div>
                        </td>
                    </tr>
                </tbody>
            </table>
            
            <!-- Error Message -->
            <div spry-if="this.error_message != null"
                 style="padding: 0.5rem; margin: 0.5rem 0; background: #f8d7da; color: #721c24; border-radius: 4px;">
                <span content-expr="this.error_message"></span>
            </div>
            
            <!-- Actions -->
            <div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
                <button type="submit" style="padding: 0.5rem 1rem; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 4px;">
                    Create User
                </button>
                <button type="button"
                        sid="cancel-btn"
                        spry-action=":CancelCreate"
                        spry-target="user-management"
                        style="padding: 0.5rem 1rem; cursor: pointer; background: #6c757d; color: white; border: none; border-radius: 4px;">
                    Cancel
                </button>
            </div>
        </form>
    </div>
</details>

HTMX Attributes and Targets

Target Strategy

All actions must properly target the correct DOM elements for HTMX swapping. The key insight is that view and edit components swap each other out using the same element ID:

Action Source Component Target Swap Result
:StartEdit UserDetailsComponent user-{user_id} outerHTML Swaps in UserDetailEditComponent
:SaveEdit UserDetailEditComponent user-{user_id} outerHTML Swaps in UserDetailsComponent - view
:CancelEdit UserDetailEditComponent user-{user_id} outerHTML Swaps in UserDetailsComponent - view
:AddPermission UserDetailEditComponent user-{user_id} outerHTML Re-renders edit component with new permission
:RemovePermission UserDetailEditComponent user-{user_id} outerHTML Re-renders edit component without permission
:DeleteUser UserDetailsComponent user-management outerHTML Refreshes entire user list
:CreateUser NewUserComponent user-management outerHTML Refreshes list with new user added
:CancelCreate NewUserComponent user-management outerHTML Hides create form
:ShowCreateUser UserManagementComponent user-management outerHTML Shows NewUserComponent

Global ID Strategy

  • UserManagementComponent: id="user-management" (static, for cross-component targeting)
  • UserDetailsComponent: id="user-{user_id}" (dynamic per user)
  • UserDetailEditComponent: id="user-{user_id}" (same ID - they swap each other)
  • NewUserComponent: id="new-user" (static)

Component Swap Flow Diagram

sequenceDiagram
    participant U as User
    participant UV as UserDetailsComponent - View
    participant UE as UserDetailEditComponent - Edit
    participant UM as UserManagementComponent
    participant S as Server/Service
    
    Note over U,S: Edit Flow - Component Swap
    U->>UV: Click Edit button
    UV->>S: POST with :StartEdit action
    S->>S: Create UserDetailEditComponent with user data
    S->>UE: Return UserDetailEditComponent - swaps in at same ID
    U->>UE: Modify fields, click Save
    UE->>S: POST with :SaveEdit action
    S->>S: Validate and save to UserService
    S->>S: Create UserDetailsComponent with updated data
    S->>UV: Return UserDetailsComponent - swaps back at same ID
    
    Note over U,S: Delete Flow
    U->>UV: Click Delete button
    UV->>S: POST with :DeleteUser action targeting user-management
    S->>S: Delete user via UserService
    S->>UM: Return refreshed UserManagementComponent
    
    Note over U,S: Create Flow
    U->>UM: Click Create User button
    UM->>S: POST with :ShowCreateUser action
    S->>UM: Return UserManagementComponent with NewUserComponent
    U->>UM: Fill form, click Create
    UM->>S: POST with :CreateUser action targeting user-management
    S->>S: Create user via UserService
    S->>UM: Return refreshed UserManagementComponent without form

Inline Editing Behavior

State Management

Each UserDetailsComponent maintains its own editing state:

public class UserDetailsComponent : Component {
    // State
    private User _user;
    public bool is_editing { get; private set; default = false; }
    public string? error_message { get; private set; default = null; }
    
    // Exposed for template
    public string user_id { get { return _user.id; } }
    public string username { get { return _user.username; } }
    public string email { get { return _user.email; } }
    public string created_at { get { return _user.created_at.format("%Y-%m-%d"); } }
    public string[] permissions { get { return _user.permissions; } }
    
    // Actions
    public void start_edit() {
        is_editing = true;
        error_message = null;
    }
    
    public void cancel_edit() {
        is_editing = false;
        error_message = null;
    }
}

Edit Toggle Flow

  1. View Mode (default): Shows read-only table with Edit/Delete buttons
  2. Click Edit: Triggers :StartEdit action, sets is_editing = true, re-renders
  3. Edit Mode: Shows form inputs in table rows with Save/Cancel buttons
  4. Save: Validates input, calls UserService, on success sets is_editing = false
  5. Cancel: Sets is_editing = false, discards changes

New User Creation Pattern

Visibility Control

The UserManagementComponent controls whether the create form is visible:

public class UserManagementComponent : Component {
    public bool show_create_form { get; private set; default = false; }
    public string? success_message { get; private set; default = null; }
    public string? error_message { get; private set; default = null; }
    
    public async override void handle_action(string action) throws Error {
        switch (action) {
            case "ShowCreateUser":
                show_create_form = true;
                break;
            case "CancelCreate":
                show_create_form = false;
                break;
            case "CreateUser":
                // Handled by NewUserDetailsComponent, but we refresh here
                show_create_form = false;
                success_message = "User created successfully";
                yield refresh_users_async();
                break;
            case "DeleteUser":
                // Handle delete, refresh list
                yield handle_delete_async();
                break;
        }
    }
}

Creation Flow

  1. User clicks "Create User" button in header
  2. UserManagementComponent sets show_create_form = true
  3. NewUserDetailsComponent renders at top of list with empty fields
  4. User fills in fields and clicks "Create User"
  5. On success: form hides, success message shows, user list refreshes
  6. On error: error message shows in form, form stays visible

CSS Removal Strategy

Before (CSS Classes)

<div class="admin-container">
    <div class="admin-header">
        <h1>User Management</h1>
        <button class="btn btn-primary">Create User</button>
    </div>
    <div class="alert alert-success">...</div>
    <table class="user-table">...</table>
</div>

After (Inline Styles)

<div style="max-width: 1200px; margin: 0 auto; padding: 1rem;">
    <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
        <h3 style="margin: 0;">User Management</h3>
        <button style="padding: 0.5rem 1rem; cursor: pointer;">Create User</button>
    </div>
    <div style="padding: 0.75rem; margin-bottom: 1rem; background: #d4edda; color: #155724; border-radius: 4px;">...</div>
    <!-- Table uses minimal inline styles -->
</div>

Style Guidelines

  1. Use minimal inline styles only for essential layout
  2. No class attributes except for semantic purposes (not styling)
  3. Prefer semantic HTML (<details>, <summary>, <table>)
  4. Use CSS custom properties if the application wants to override defaults:

    <div style="background: var(--spry-alert-success-bg, #d4edda);">
    

API/Endpoint Requirements

No New Endpoints Required

The redesign uses the existing Spry component action pattern:

  • Actions are handled within components via handle_action()
  • No new HTTP endpoints need to be created
  • Existing UserService and PermissionService methods are sufficient

Service Dependencies

Components continue to use:

  • UserService: create_user_async, get_user_async, update_user_async, delete_user_async, list_users_async
  • PermissionService: set_permission_async, has_permission_by_id_async
  • SessionService: authenticate_request_async (for permission checks)
  • ComponentFactory: create child components

Potential Future Enhancements

  1. Bulk Operations: Add delete_users_async(string[] ids) for multi-select delete
  2. Search API: Add search_users_async(string query) to UserService for efficient searching
  3. Audit Logging: Add methods to track who changed what and when

Implementation Checklist

Files to Create

  • src/Authentication/Components/UserManagementComponent.vala - New container component
  • src/Authentication/Components/UserDetailsComponent.vala - View-only details/summary component
  • src/Authentication/Components/UserDetailEditComponent.vala - Edit mode with integrated permissions
  • src/Authentication/Components/NewUserComponent.vala - Create form with integrated permissions

Files to Modify

  • src/Authentication/meson.build - Add new component files, remove old ones
  • examples/UsersExample.vala - Update to use new component

Files to Deprecate/Remove

  • src/Authentication/Components/UserManagementPage.vala - Replace with UserManagementComponent
  • src/Authentication/Components/UserListComponent.vala - Merged into UserManagementComponent
  • src/Authentication/Components/UserListItemComponent.vala - Replaced by UserDetailsComponent
  • src/Authentication/Components/UserFormComponent.vala - Functionality moved to UserDetailEditComponent and NewUserComponent
  • src/Authentication/Components/PermissionEditorComponent.vala - Functionality integrated directly into edit/create components

Migration Guide for Applications

Before (using UserManagementPage)

// Register as a page - generates full HTML document
application.add_transient<UserManagementPage>();
application.add_endpoint<UserManagementPage>(new EndpointRoute("/admin/users"));

After (using UserManagementComponent)

// Option 1: Use in your own PageComponent
public class AdminPage : PageComponent {
    public override string markup {
        return """
        <!DOCTYPE html>
        <html>
        <head>...</head>
        <body>
            <nav>...</nav>
            <main>
                <!-- Place component anywhere in your layout -->
                <spry-component name="Spry.Authentication.Components.UserManagementComponent" sid="user-mgmt"/>
            </main>
        </body>
        </html>
        """;
    }
}

// Option 2: Create a simple wrapper page
public class UserAdminPage : PageComponent {
    private ComponentFactory _factory = inject<ComponentFactory>();
    
    public override string markup {
        return """
        <!DOCTYPE html>
        <html>
        <head>
            <title>User Management</title>
            <script spry-res="htmx.js"></script>
        </head>
        <body>
            <spry-outlet sid="content"/>
        </body>
        </html>
        """;
    }
    
    public override async void prepare() throws Error {
        var component = _factory.create<UserManagementComponent>();
        add_outlet_child("content", component);
        add_globals_from(component);
    }
}

Summary

This redesign simplifies the user management component architecture from 5 components to 3, removes all CSS class dependencies, and implements a modern <details>/<summary> pattern with inline editing. The key benefits are:

  1. Flexibility: Applications can place UserManagementComponent anywhere in their layout
  2. No Style Conflicts: Inline styles avoid CSS class collisions
  3. Better UX: Inline editing is more intuitive than modal forms
  4. Maintainability: Fewer components with clearer responsibilities
  5. Consistency: Same editable table pattern for both editing and creating