AI Skill Report Card

Generating Select Components

B+78·May 7, 2026·Source: Web

Quick Start

Provide entity name in kebab-case (e.g., banco, centro-custo, sindicato) and I'll generate both files:

  1. src/models/selects/select-<entity>.ts — entity-specific model
  2. src/components/selects/select-<entity>.tsx — component with search, infinite scroll, multiple selection

Example: For "sindicato" entity:

  • Model: src/models/selects/select-sindicato.ts
  • Component: src/components/selects/select-sindicato.tsx
  • Usage: useModel('selects.select-sindicato')
14 / 15

Step 1: Identify Entity Information

  • Entity name (kebab-case)
  • Controller function name and import path
  • API endpoint for text search
  • Label mapping (r.label or composite like ${r.codigo} - ${r.nome})
  • Sort field (order_by)

Step 2: Generate Model

  • Create src/models/selects/select-<entity>.ts
  • Import specific controller
  • Set correct order_by field
  • Add external parameters if needed (e.g., versaoRelatorioId)

Step 3: Generate Component

  • Create src/components/selects/select-<entity>.tsx
  • Set correct useModel hook
  • Replace ENDPOINT placeholder
  • Configure label mapping in optionsMemo/optionsSearched
  • Add dependency handling if needed

Step 4: Verify Integration

  • Model returns 6 standard fields
  • Component accepts standard props
  • Search debounce works (700ms)
  • Infinite scroll implemented
  • Missing options filling works
Recommendation
Show actual input/output pairs in examples - instead of 'Input: Entity sindicato', show the exact entity name given and the complete generated files
12 / 20

Example 1: Simple Entity (Sindicato) Input: Entity "sindicato", controller "getSindicatosController", endpoint "/api/pessoal/sindicatos/"

Output:

TypeScript
// Model uses: getSindicatosController, order_by: 'label' // Component uses: useModel('selects.select-sindicato'), r.label

Example 2: Composite Label (Colaborador) Input: Entity "colaborador", composite label with code and name

Output:

TypeScript
// Label mapping: `${r.codigo} - ${r.nome}` // order_by: 'codigo'

Example 3: With External Dependency Input: Entity needs versaoRelatorioId parameter

Output:

TypeScript
// Model: export default (versaoRelatorioId?: number) => { ... } // Component: useUpdateEffect to reset when dependency changes
Recommendation
Reduce template verbosity by focusing on the key customization points rather than showing full 100+ line templates
TypeScript
import { defaultPaginationValues } from '@/constants/pagination'; import useLoading from '@/hooks/use-loading'; import { getUpdateSelectOptions } from '@/utils/get-update-select-options'; import { message } from 'antd'; import { useState } from 'react'; // REPLACE: Import specific controller // import { get<Entity>Controller } from '<controller-path>'; export default () => { const [options, setOptions] = useState<App.Pagination<EntityType>>( defaultPaginationValues, ); const [queryStringOptionsParams, setQueryStringOptionsParams] = useState<App.QueryStringParams>({ page: 1, page_size: 10, order_by: 'label', // REPLACE: Use appropriate sort field }); const { loading, startLoading, stopLoading } = useLoading(); const addMissedOptions = async (ids: number[]) => { try { startLoading(); const params: App.QueryStringParams = { ...queryStringOptionsParams, page: 1, page_size: 1000, id: ids, }; // REPLACE: Use specific controller const response = await get<Entity>Controller(params); const { data } = response; setOptions((previousData) => { const results: any[] = getUpdateSelectOptions( data.results, previousData.results, ); return { ...previousData, results }; }); } catch (error) { message.error('Não foi possível obter os dados'); } finally { stopLoading(); } }; const getOptions = async (...args: Parameters<typeof get<Entity>Controller>) => { try { startLoading(); const response = await get<Entity>Controller(...args); const { data } = response; setOptions((previousData) => { const results: any[] = getUpdateSelectOptions( data.results, previousData.results, ); return { ...data, results }; }); } catch (error) { message.error('Não foi possível obter os dados'); return Promise.reject(); } finally { stopLoading(); } }; const getOptionsNewPage = async () => { try { startLoading(); if (!(queryStringOptionsParams.page + 1 <= options.total_pages)) return; const params: App.QueryStringParams = { ...queryStringOptionsParams, page: queryStringOptionsParams.page + 1, page_size: queryStringOptionsParams.page_size, }; const response = await get<Entity>Controller(params); const { data } = response; setOptions((previousData) => { const results: any[] = getUpdateSelectOptions( data.results, previousData.results, ); return { ...data, results }; }); setQueryStringOptionsParams(params); } catch (error) { message.error('Não foi possível obter os dados'); return Promise.reject(); } finally { stopLoading(); } }; return { selectLoading: loading, addMissedOptions, options, getOptions, getOptionsNewPage, queryStringOptionsParams, }; };
TypeScript
import { defaultPaginationValues } from '@/constants/pagination'; import useLoading from '@/hooks/use-loading'; import { getSelectController } from '@/services/select.controller'; import { getUpdateSelectOptions } from '@/utils/get-update-select-options'; import { useModel } from '@umijs/max'; import { Select, SelectProps } from 'antd'; import { message } from 'antd/lib'; import { debounce } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; interface Props { value?: any; onChange?: (val: any, option: any) => void; multiple?: boolean; allowClear?: boolean; style?: any; placeholder?: string; disabled?: boolean; labelInValue?: boolean; isInstanceForColumnFilter?: boolean; } export default (props: Props) => { const [needFillMissedOptions, setNeedFillMissedOptions] = useState<boolean>(true); const [searchValue, setSearchValue] = useState<string>(); const [isSearching, setIsSearching] = useState<boolean>(false); const [searchedOptions, setSearchedOptions] = useState<App.Pagination<EntityType>>( defaultPaginationValues, ); const [queryStringSearchedOptionsParams, setQueryStringSearchedOptionsParams] = useState({ reset: true, params: { page: 1, page_size: 10, order_by: 'label', // REPLACE: Use same as model }, }); // REPLACE: Use specific entity model const { selectLoading, options, getOptions, getOptionsNewPage, queryStringOptionsParams, addMissedOptions, } = useModel('selects.select-<entity>'); const { loading, startLoading, stopLoading } = useLoading(); // Search functionality with debounce const getSearchedOptions = async (reset: boolean, ...args: Parameters<typeof getSelectController>) => { try { startLoading(); const response = await getSelectController(...args); const { data } = response; if (reset) setSearchedOptions(data); else { setSearchedOptions((previousData) => { const results: any[] = getUpdateSelectOptions(data.results, previousData.results); return { ...data, results }; }); } } catch (error) { message.error('Não foi possível obter os dados'); return Promise.reject(); } finally { stopLoading(); } }; const debouncedSearch = useMemo( () => debounce((value: string) => { if (value) { setIsSearching(true); const params = { reset: true, params: { page: 1, page_size: 10, order_by: 'label', search: value }, }; setQueryStringSearchedOptionsParams(params); // REPLACE: Use specific endpoint getSearchedOptions(params.reset, 'ENDPOINT_HERE', params.params); } else { setIsSearching(false); } }, 700), [], ); // Options mapping const optionsMemo = useMemo(() => { return (options.results || []).map((r: any) => ({ // REPLACE: Use r.label or composite like `${r.codigo} - ${r.nome}` label: r.label, value: r.id, })); }, [options.results]); const optionsSearched = useMemo(() => { return (searchedOptions.results || []).map((r: any) => ({ // REPLACE: Same label mapping as optionsMemo label: r.label, value: r.id, })); }, [searchedOptions.results]); // Fill missing options when value is pre-populated const { value } = props; useEffect(() => { const getMissed = async () => { const optionsIds = new Set(optionsMemo.map((o) => o.value)); const values = Array.isArray(value) ? value : [value]; if (values.some((v) => !optionsIds.has(v))) { setNeedFillMissedOptions(false); await addMissedOptions(values); } }; if ((props?.multiple ? value?.length > 0 : value) && needFillMissedOptions) { if (props.labelInValue) { const values = Array.isArray(value) ? [value?.at(0)?.value] : [value?.value as number]; const optionsIds = new Set(optionsMemo.map((o) => o.value)); if (values.some((v) => !optionsIds.has(v))) { setNeedFillMissedOptions(false); addMissedOptions(values); } } else getMissed(); } }, [value]); return ( <Select searchValue={searchValue} onSearch={(value) => { setSearchValue(value); debouncedSearch(value); }} options={isSearching ? optionsSearched : optionsMemo} placeholder={props.placeholder || 'Selecione'} style={{ width: '100%', ...props.style }} showSearch optionFilterProp="label" onPopupScroll={(e) => { const target = e.target as HTMLDivElement; if (target.scrollTop + target.clientHeight >= target.scrollHeight) { if (isSearching) { // Handle searched options pagination } else { getOptionsNewPage(); } } }} loading={selectLoading || loading} value={props.value} onChange={props.onChange} onFocus={() => { if (isSearching) { if (searchedOptions.total_pages === 0) { // REPLACE: Use specific endpoint getSearchedOptions(true, 'ENDPOINT_HERE', queryStringSearchedOptionsParams.params); } } else { if (options.total_pages === 0) { getOptions(queryStringOptionsParams); } } }} mode={props.multiple ? 'multiple' : undefined} allowClear={props.allowClear} maxTagCount={3} disabled={props.disabled} labelInValue={props.labelInValue} {...(props.isInstanceForColumnFilter && { getPopupContainer: (trigger) => trigger.parentElement, listHeight: 200, })} /> ); };
  • Entity-specific models: Never share models between entities
  • Standard returns: Models always return the same 6 fields
  • Infinite scroll: Implement for both default and search results
  • Debounced search: 700ms delay prevents excessive API calls
  • Missing options: Auto-fill when value is pre-populated
  • Column filters: Use isInstanceForColumnFilter for table integration
  • Error handling: Show user-friendly messages on API errors
  • Don't create generic shared models - each entity needs its own
  • Don't forget to replace placeholder values (ENDPOINT_HERE, order_by, label mapping)
  • Don't skip the missing options filling logic - it's crucial for pre-populated forms
  • Don't use the wrong useModel hook - must match the entity name exactly
  • Don't forget external dependencies when the controller requires additional parameters
0
Grade B+AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
14/15
Workflow
14/15
Examples
12/20
Completeness
12/20
Format
14/15
Conciseness
12/15