Vyuh CDX
Configuration

Entity Configuration

Complete guide to EntityConfiguration class and bringing all entity aspects together

The EntityConfiguration class is the heart of the Vyuh Entity System. It brings together all aspects of an entity - from metadata and API to layouts and forms - into a single, cohesive configuration. This guide provides an in-depth look at every aspect of entity configuration.

Overview

class EntityConfiguration<T extends EntityBase> {
  final EntityMetadata metadata;
  final EntityApi<T> api;
  final EntityLayoutDescriptor<T> layouts;
  final EntityFormDescriptor<T> form;
  final EntityActions<T>? actions;
  final RouteBuilder? routeBuilder;

  // ... constructor and methods
}

Core Components

1. EntityMetadata

The metadata describes your entity and controls how it appears throughout the system.

class EntityMetadata {
  final String identifier;        // Unique identifier (e.g., 'users', 'products')
  final String name;              // Singular display name
  final String pluralName;        // Plural display name
  final String? description;      // Entity description
  final IconData? icon;           // Display icon
  final Color? themeColor;        // Theme color for UI elements
  final String? category;         // Menu category
  final EntityRouteBuilder route; // Route configuration
  final Future<String?> Function(BuildContext)? getHelp; // Help provider
}

Example Configuration

EntityMetadata(
  identifier: 'reference_standards',
  name: 'Reference Standard',
  pluralName: 'Reference Standards',
  description: 'Chemical and biological reference materials',
  icon: Icons.science,
  themeColor: Colors.purple,
  category: 'Laboratory',
  route: EntityRouteBuilder.fromIdentifier('reference-standards'),
  getHelp: (context) async {
    return '''
    Reference standards are certified materials used for:
    - Instrument calibration
    - Method validation
    - Quality control
    ''';
  },
)

2. API Configuration

The API configuration provides a factory function that creates an EntityApi instance:

api: (client) => ReferenceStandardApi(client: client),

Your API class should extend EntityApi<T>:

class ReferenceStandardApi extends EntityApi<ReferenceStandard> {
  ReferenceStandardApi({required super.client});

  @override
  ReferenceStandard fromJson(Map<String, dynamic> json) =>
      ReferenceStandard.fromJson(json);

  // Optional: Override base methods for custom behavior
  @override
  Future<List<ReferenceStandard>> performList({
    int? offset,
    int? limit,
    String? sortBy,
    String? sortOrder,
    String? search,
  }) async {
    // Custom list implementation
    final response = await client.get(
      '/api/v1/reference-standards',
      queryParameters: {
        if (offset != null) 'offset': offset.toString(),
        if (limit != null) 'limit': limit.toString(),
        if (sortBy != null) 'sortBy': sortBy,
        if (sortOrder != null) 'sortOrder': sortOrder,
        if (search != null) 'search': search,
      },
    );

    final List<dynamic> data = response.data['data'];
    return data.map((json) => fromJson(json)).toList();
  }
}

3. Layout Configuration

The EntityLayoutDescriptor defines how your entity is displayed in different contexts:

class EntityLayoutDescriptor<T> {
  final List<EntityLayout<T>> list;         // List view layouts
  final List<EntityLayout<T>> details;      // Detail view layouts
  final List<EntityLayout<T>>? summary;     // Summary/card layouts
  final List<EntityLayout<T>>? dashboard;   // Dashboard view layouts
  final List<EntityLayout<T>>? analytics;   // Analytics contributions
}

List Layouts

list: [
  // Table layout for desktop
  TableListLayout<User>(
    title: 'Users',
    columns: [
      TableColumn(
        key: 'name',
        label: 'Name',
        getValue: (user) => user.name,
        sortable: true,
        searchable: true,
      ),
      TableColumn(
        key: 'email',
        label: 'Email',
        getValue: (user) => user.email,
        sortable: true,
      ),
      TableColumn(
        key: 'role',
        label: 'Role',
        getValue: (user) => user.role.toUpperCase(),
        width: 100,
      ),
      TableColumn(
        key: 'status',
        label: 'Status',
        getValue: (user) => user.isActive ? 'Active' : 'Inactive',
        buildCell: (context, user) => Chip(
          label: Text(user.isActive ? 'Active' : 'Inactive'),
          backgroundColor: user.isActive ? Colors.green : Colors.grey,
        ),
      ),
    ],
    onRowTap: (context, user) {
      // Navigate to detail view
      context.go('/users/${user.id}');
    },
    bulkActions: [
      BulkAction(
        label: 'Activate',
        icon: Icons.check,
        onTap: (context, users) async {
          // Bulk activate logic
        },
      ),
    ],
  ),

  // Grid layout for mobile/tablet
  GridListLayout<User>(
    title: 'Users',
    crossAxisCount: 2,
    buildCard: (context, user) => Card(
      child: InkWell(
        onTap: () => context.go('/users/${user.id}'),
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              CircleAvatar(
                child: Text(user.name[0]),
              ),
              const SizedBox(height: 8),
              Text(
                user.name,
                style: Theme.of(context).textTheme.titleMedium,
              ),
              Text(
                user.email,
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ],
          ),
        ),
      ),
    ),
  ),
]

Detail Layouts

details: [
  // Standard detail layout
  EntityLayoutConfiguration<User>(
    canHandle: (context) => true, // Always use this layout
    build: (context, user) => SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Header section
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Row(
                children: [
                  CircleAvatar(
                    radius: 40,
                    child: Text(user.name[0]),
                  ),
                  const SizedBox(width: 16),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          user.name,
                          style: Theme.of(context).textTheme.headlineMedium,
                        ),
                        Text(user.email),
                        Chip(
                          label: Text(user.role),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          ),

          // Information sections
          const SizedBox(height: 24),
          _buildSection(
            context,
            title: 'Contact Information',
            children: [
              _buildInfoRow('Phone', user.phone),
              _buildInfoRow('Department', user.department),
              _buildInfoRow('Location', user.location),
            ],
          ),

          // Related entities
          const SizedBox(height: 24),
          _buildSection(
            context,
            title: 'Permissions',
            children: [
              FutureBuilder<List<Permission>>(
                future: _loadUserPermissions(user.id),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) {
                    return const CircularProgressIndicator();
                  }
                  return Wrap(
                    spacing: 8,
                    children: snapshot.data!.map((permission) {
                      return Chip(label: Text(permission.name));
                    }).toList(),
                  );
                },
              ),
            ],
          ),
        ],
      ),
    ),
  ),
]

4. Form Configuration

The EntityFormDescriptor handles form creation and data transformation:

abstract class EntityFormDescriptor<T> {
  StepForm prepare(T? entity);
  Map<String, dynamic> toFormData(T entity);
  T fromFormData(Map<String, dynamic> data, {T? entity});
}

Complex Form Example

class UserFormDescriptor extends EntityFormDescriptor<User> {
  @override
  StepForm prepare(User? entity) {
    final isEdit = entity != null;

    return StepForm(
      title: isEdit ? 'Edit User' : 'Create User',
      steps: [
        FormStep(
          title: 'Basic Information',
          form: FormBuilder(
            title: 'User Details',
            fields: [
              TextField(
                name: 'name',
                label: 'Full Name',
                value: entity?.name,
                validators: [
                  required(),
                  minLength(3),
                  maxLength(100),
                ],
              ),
              TextField(
                name: 'email',
                label: 'Email Address',
                value: entity?.email,
                validators: [
                  required(),
                  email(),
                  // Custom async validator
                  AsyncValidator(
                    validator: (value) async {
                      if (isEdit && value == entity.email) return null;
                      final exists = await checkEmailExists(value);
                      return exists ? 'Email already in use' : null;
                    },
                  ),
                ],
              ),
              SelectField(
                name: 'role',
                label: 'Role',
                value: entity?.role,
                options: [
                  Option(value: 'admin', label: 'Administrator'),
                  Option(value: 'manager', label: 'Manager'),
                  Option(value: 'user', label: 'Standard User'),
                ],
                validators: [required()],
              ),
            ],
          ),
        ),
        FormStep(
          title: 'Department & Location',
          form: FormBuilder(
            title: 'Assignment',
            fields: [
              SelectField(
                name: 'department',
                label: 'Department',
                value: entity?.department,
                optionsBuilder: () async {
                  final departments = await loadDepartments();
                  return departments.map((d) =>
                    Option(value: d.id, label: d.name)
                  ).toList();
                },
                validators: [required()],
              ),
              SelectField(
                name: 'location',
                label: 'Location',
                value: entity?.location,
                dependsOn: ['department'],
                optionsBuilder: (formData) async {
                  final deptId = formData['department'];
                  if (deptId == null) return [];

                  final locations = await loadLocations(deptId);
                  return locations.map((l) =>
                    Option(value: l.id, label: l.name)
                  ).toList();
                },
              ),
              TextField(
                name: 'phone',
                label: 'Phone Number',
                value: entity?.phone,
                validators: [
                  pattern(r'^\+?[\d\s-()]+$', 'Invalid phone number'),
                ],
              ),
            ],
          ),
        ),
        FormStep(
          title: 'Permissions',
          form: FormBuilder(
            title: 'Access Control',
            fields: [
              MultiSelectField(
                name: 'permissions',
                label: 'Permissions',
                value: entity?.permissions,
                optionsBuilder: () async {
                  final permissions = await loadAvailablePermissions();
                  return permissions.map((p) =>
                    Option(
                      value: p.id,
                      label: p.name,
                      description: p.description,
                    )
                  ).toList();
                },
              ),
              ToggleField(
                name: 'isActive',
                label: 'Active Account',
                value: entity?.isActive ?? true,
                description: 'Inactive accounts cannot log in',
              ),
            ],
          ),
        ),
      ],
    );
  }

  @override
  Map<String, dynamic> toFormData(User entity) {
    return {
      'name': entity.name,
      'email': entity.email,
      'role': entity.role,
      'department': entity.department,
      'location': entity.location,
      'phone': entity.phone,
      'permissions': entity.permissions,
      'isActive': entity.isActive,
    };
  }

  @override
  User fromFormData(Map<String, dynamic> data, {User? entity}) {
    return User(
      id: entity?.id ?? '',
      schemaType: 'users',
      name: data['name'],
      email: data['email'],
      role: data['role'],
      department: data['department'],
      location: data['location'],
      phone: data['phone'],
      permissions: List<String>.from(data['permissions'] ?? []),
      isActive: data['isActive'],
      createdAt: entity?.createdAt ?? DateTime.now(),
    );
  }
}

5. Entity Actions

Actions provide custom operations for single entities or bulk operations:

class EntityActions<T> {
  final List<EntityAction<T>>? list;    // Actions for list view
  final List<EntityAction<T>>? single;  // Actions for single entity
  final List<EntityAction<T>>? bulk;    // Bulk actions
}

class EntityAction<T> {
  final String label;
  final IconData? icon;
  final Future<void> Function(BuildContext, List<T>) onTap;
  final bool Function(List<T>)? isEnabled;
  final bool requiresConfirmation;
}

Action Examples

actions: EntityActions<Equipment>(
  list: [
    EntityAction(
      label: 'Import CSV',
      icon: Icons.upload_file,
      onTap: (context, _) async {
        final file = await pickFile();
        if (file != null) {
          await importEquipmentFromCsv(file);
          if (context.mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Import completed')),
            );
          }
        }
      },
    ),
  ],
  single: [
    EntityAction(
      label: 'Generate QR Code',
      icon: Icons.qr_code,
      onTap: (context, entities) async {
        final equipment = entities.first;
        final qrCode = await generateQrCode(equipment);
        if (context.mounted) {
          showDialog(
            context: context,
            builder: (_) => QrCodeDialog(qrCode: qrCode),
          );
        }
      },
    ),
    EntityAction(
      label: 'Schedule Maintenance',
      icon: Icons.calendar_today,
      isEnabled: (entities) =>
        entities.first.status == EquipmentStatus.operational,
      onTap: (context, entities) async {
        final equipment = entities.first;
        await showMaintenanceScheduler(context, equipment);
      },
    ),
  ],
  bulk: [
    EntityAction(
      label: 'Assign to Location',
      icon: Icons.location_on,
      requiresConfirmation: true,
      onTap: (context, entities) async {
        final location = await showLocationPicker(context);
        if (location != null) {
          await assignEquipmentToLocation(entities, location);
        }
      },
    ),
  ],
),

6. Custom Route Builder

For advanced routing needs, provide a custom route builder:

routeBuilder: (configuration) {
  return (BuildContext context, GoRouterState state) {
    // Custom route handling
    final entityId = state.pathParameters['id'];

    // Example: Add animation
    return CustomTransitionPage(
      child: EntityDetailView<Equipment>(
        configuration: configuration,
        entityId: entityId!,
      ),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return FadeTransition(
          opacity: animation,
          child: child,
        );
      },
    );
  };
},

Complete Configuration Example

Here's a comprehensive example bringing all components together:

class LocationConfig {
  static final instance = EntityConfiguration<Location>(
    metadata: EntityMetadata(
      identifier: 'locations',
      name: 'Location',
      pluralName: 'Locations',
      description: 'Physical locations and facilities',
      icon: Icons.location_city,
      themeColor: Colors.teal,
      category: 'Organization',
      route: EntityRouteBuilder.fromIdentifier('locations'),
      getHelp: (context) async =>
        await loadHelpContent('locations'),
    ),

    api: (client) => LocationApi(client: client),

    layouts: EntityLayoutDescriptor(
      list: [
        TableListLayout<Location>(
          title: 'Locations',
          columns: _buildTableColumns(),
          filters: _buildFilters(),
          defaultSort: 'name',
        ),
        LocationMapLayout(), // Custom map view
      ],
      details: [
        LocationDetailLayout(),
        LocationFloorPlanLayout(), // Custom floor plan view
      ],
      summary: [
        LocationCardLayout(),
      ],
      dashboard: [
        LocationStatsWidget(),
        LocationUtilizationChart(),
      ],
    ),

    form: LocationFormDescriptor(),

    actions: EntityActions<Location>(
      list: [
        EntityAction(
          label: 'Import Locations',
          icon: Icons.upload,
          onTap: _handleImport,
        ),
      ],
      single: [
        EntityAction(
          label: 'View Floor Plan',
          icon: Icons.map,
          onTap: _showFloorPlan,
        ),
        EntityAction(
          label: 'Generate Report',
          icon: Icons.description,
          onTap: _generateReport,
        ),
      ],
    ),
  );
}

Advanced Configuration Patterns

Dynamic Configuration

Configuration can be dynamic based on user permissions or context:

EntityConfiguration<Document>(
  // ... other config
  layouts: EntityLayoutDescriptor(
    list: [
      // Show different layouts based on user role
      if (userRole == 'admin')
        AdminDocumentListLayout()
      else
        StandardDocumentListLayout(),
    ],
  ),
  actions: EntityActions(
    single: [
      // Conditionally add actions
      if (hasPermission('documents.approve'))
        EntityAction(
          label: 'Approve',
          icon: Icons.check,
          onTap: _approveDocument,
        ),
    ],
  ),
)

Configuration Composition

Build configurations from reusable components:

class BaseEntityConfig {
  static EntityLayoutDescriptor<T> createStandardLayouts<T>() {
    return EntityLayoutDescriptor<T>(
      list: [
        TableListLayout<T>(...),
        GridListLayout<T>(...),
      ],
      details: [
        StandardDetailLayout<T>(),
      ],
    );
  }

  static EntityActions<T> createStandardActions<T>() {
    return EntityActions<T>(
      list: [
        EntityAction(
          label: 'Export',
          icon: Icons.download,
          onTap: (context, entities) => exportEntities(entities),
        ),
      ],
    );
  }
}

// Use in specific configurations
final config = EntityConfiguration<Product>(
  // ... metadata and api
  layouts: BaseEntityConfig.createStandardLayouts<Product>(),
  actions: BaseEntityConfig.createStandardActions<Product>(),
  form: ProductFormDescriptor(),
);

Best Practices

  1. Keep configurations focused - Each configuration should handle one entity type
  2. Use type safety - Leverage Dart's type system for compile-time safety
  3. Provide multiple layouts - Different users prefer different views
  4. Include help content - Use the getHelp function for context-sensitive help
  5. Test configurations - Unit test form descriptors and API implementations
  6. Document custom behaviors - Comment any non-standard implementations

Next: Entity Metadata - Deep dive into entity metadata configuration