All files / src/components/shared/OrganizerSelect OrganizerSelect.tsx

75.75% Statements 25/33
67.85% Branches 19/28
55.55% Functions 5/9
82.14% Lines 23/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158                                                                                  9x                     57x     57x                   57x 25x 25x             57x     57x 49x 28x               21x   21x               21x 20x             21x               21x       57x         57x                                     9x 2x         2x 2x             2x    
/* eslint-disable react-refresh/only-export-components */
/**
 * OrganizerSelect Component
 *
 * Shared dropdown component for selecting organizers
 * Fetches users with ORGANIZER role from the API
 *
 * Used in:
 * - EventTasksTab (Story 5.5) - Assign tasks to organizers
 * - SpeakerOutreachDashboard (Story 5.3) - Filter speakers by assigned organizer
 */
 
import React from 'react';
import {
  FormControl,
  InputLabel,
  Select,
  MenuItem,
  CircularProgress,
  FormControlProps,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useUserList } from '@/hooks/useUserManagement';
import type { User } from '@/types/user.types';
 
export interface Organizer {
  id: string;
  name: string;
}
 
interface OrganizerSelectProps extends Omit<FormControlProps, 'onChange'> {
  value: string;
  onChange: (organizerId: string) => void;
  organizers?: Organizer[]; // Optional - if not provided, will fetch from API
  includeUnassigned?: boolean;
  includeAllOption?: boolean;
  label?: string;
  disabled?: boolean;
  size?: 'small' | 'medium';
}
 
export const OrganizerSelect: React.FC<OrganizerSelectProps> = ({
  value,
  onChange,
  organizers: organizersProp,
  includeUnassigned = true,
  includeAllOption = false,
  label,
  disabled = false,
  size = 'small',
  ...formControlProps
}) => {
  const { t } = useTranslation('common');
 
  // Fetch users with ORGANIZER role (only if not provided via props)
  const { data: organizersData, isLoading } = useUserList({
    filters: { role: ['ORGANIZER'] },
    pagination: { page: 1, limit: 100 },
    enabled: !organizersProp, // Only fetch if organizers not provided
  });
 
  // Use prop organizers if provided, otherwise transform from API
  // CRITICAL: Must depend on organizersData.data (not organizersData) to prevent infinite re-renders
  // The organizersData object itself is a new reference on every render, but organizersData.data
  // only changes when the actual data changes, preventing unnecessary recalculations
  const organizers: Organizer[] = React.useMemo(() => {
    Iif (organizersProp) return organizersProp;
    Eif (!organizersData?.data) return [];
    return organizersData.data.map((user: User) => ({
      id: user.id, // id field IS the username (e.g., "john.doe")
      name: `${user.firstName} ${user.lastName}`.trim() || user.id,
    }));
  }, [organizersProp, organizersData?.data]); // Fixed: organizersData -> organizersData?.data
 
  const labelText = label || t('organizer.select', 'Select Organizer');
 
  // Build menu items as an array (MUI Select requires array, not Fragment)
  const menuItems = React.useMemo(() => {
    if (isLoading) {
      return [
        <MenuItem key="loading" disabled>
          <CircularProgress size={20} sx={{ mr: 1 }} />
          {t('loading', 'Loading...')}
        </MenuItem>,
      ];
    }
 
    const items = [];
 
    Iif (includeAllOption) {
      items.push(
        <MenuItem key="all" value="all" data-testid="organizer-option-all">
          {t('organizer.all', 'All Organizers')}
        </MenuItem>
      );
    }
 
    if (includeUnassigned) {
      items.push(
        <MenuItem key="unassigned" value="" data-testid="organizer-option-unassigned">
          <em>{t('organizer.unassigned', 'Unassigned')}</em>
        </MenuItem>
      );
    }
 
    organizers.forEach((org) => {
      items.push(
        <MenuItem key={org.id} value={org.id} data-testid={`organizer-option-${org.id}`}>
          {org.name}
        </MenuItem>
      );
    });
 
    return items;
  }, [isLoading, includeAllOption, includeUnassigned, organizers, t]);
 
  // Extract data-testid from formControlProps to avoid duplication
  const { 'data-testid': testId, ...restFormControlProps } = formControlProps as Record<
    string,
    unknown
  >;
 
  return (
    <FormControl size={size} disabled={disabled || isLoading} {...restFormControlProps}>
      <InputLabel>{labelText}</InputLabel>
      <Select
        value={isLoading ? '' : value || ''}
        onChange={(e) => onChange(e.target.value)}
        label={labelText}
        data-testid={testId}
        // DON'T use inputProps for test-id - it applies to hidden native input!
      >
        {menuItems}
      </Select>
    </FormControl>
  );
};
 
/**
 * Hook to get organizers list (for non-dropdown usage)
 */
export const useOrganizers = () => {
  const { data: organizersData, isLoading } = useUserList({
    filters: { role: ['ORGANIZER'] },
    pagination: { page: 1, limit: 100 },
  });
 
  const organizers: Organizer[] = React.useMemo(() => {
    Eif (!organizersData?.data) return [];
    return organizersData.data.map((user: User) => ({
      id: user.id, // id field IS the username (e.g., "john.doe")
      name: `${user.firstName} ${user.lastName}`.trim() || user.id,
    }));
  }, [organizersData?.data]); // Fixed: organizersData -> organizersData?.data
 
  return { organizers, isLoading };
};