AI Skill Report Card

Generating Select Components

B-72·May 7, 2026·Source: Web
12 / 15

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

  1. src/models/selects/select-<entity>.ts — dedicated model with states, loading, pagination, search, infinite scroll
  2. src/components/selects/select-<entity>.tsx — Select component with search, infinite scroll, multiple support

Example:

Entity: sindicato
Controller: getSindicatosController
Endpoint: /api/pessoal/sindicatos/
Recommendation
Reduce verbosity in workflow steps - the code blocks are extremely long and could be condensed to key patterns rather than full implementations
13 / 15

Step 1: Entity Identification

  • Check attachments for typings.d.ts or entity definitions
  • Look for entity name in user's current file/selection
  • If unclear, ask for: entity name (kebab-case), controller function, API endpoint, label mapping

Step 2: Generate Model (src/models/selects/select-<entity>.ts)

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'; 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', // or appropriate 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, }; 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, }; };

Step 3: Generate Component (src/components/selects/select-<entity>.tsx)

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 { selectLoading, options, getOptions, getOptionsNewPage, queryStringOptionsParams, addMissedOptions, } = useModel('selects.select-<entity>'); const { loading, startLoading, stopLoading } = useLoading(); // Search functionality with debounce const debouncedSearch = useMemo( () => debounce((value: string) => { if (value) { setIsSearching(true); getSearchedOptions(true, '<API_ENDPOINT>', { page: 1, page_size: 10, order_by: 'label', search: value, }); } else { setIsSearching(false); } }, 700), [], ); // Infinite scroll handlers const handleScroll = (e: React.UIEvent<HTMLDivElement>) => { const target = e.target as HTMLDivElement; if (target.scrollTop + target.clientHeight >= target.scrollHeight) { if (queryStringOptionsParams.page + 1 <= options.total_pages) { getOptionsNewPage(); } } }; // Fill missing options for pre-selected values useEffect(() => { if ((props?.multiple ? props.value?.length > 0 : props.value) && needFillMissedOptions) { const optionsIds = new Set(options.results?.map((o) => o.id)); const values = Array.isArray(props.value) ? props.value : [props.value]; if (values.some((v) => !optionsIds.has(v))) { setNeedFillMissedOptions(false); addMissedOptions(values); } } }, [props.value, options.results]); const optionsMemo = useMemo(() => { return (options.results || []).map((r: any) => ({ label: r.label, // or composite: `${r.codigo} - ${r.nome}` value: r.id, })); }, [options.results]); return ( <Select searchValue={searchValue} onSearch={(value) => { setSearchValue(value); debouncedSearch(value); }} options={isSearching ? searchedOptions : optionsMemo} placeholder={props.placeholder || 'Selecione'} style={{ width: '100%', ...props.style }} showSearch optionFilterProp="label" onPopupScroll={handleScroll} loading={selectLoading || loading} value={props.value} onChange={props.onChange} onFocus={() => { if (!isSearching && options.total_pages === 0) { getOptions(queryStringOptionsParams); } }} mode={props.multiple ? 'multiple' : undefined} allowClear={props.allowClear} maxTagCount={3} disabled={props.disabled} labelInValue={props.labelInValue} /> ); };

Step 4: Cleanup Entity Model (CRITICAL)

After creating the select model, MUST remove from the entity model (src/pages/.../model.ts):

Progress:

  • Remove *Options state (e.g., contasContabeisOptions)
  • Remove queryString*OptionsParams state
  • Remove get*Options, get*OptionsNewPage, addMissed*Options functions
  • Remove getUpdateSelectOptions import (if only used for select)
  • Remove options updates in CRUD operations (create, update, delete)
  • Remove select-related exports from model return
Recommendation
Make examples more concrete with actual input/output pairs showing the generated file contents for specific entities
14 / 20

Example 1: Simple Label Input: Entity "sindicato", controller "getSindicatosController", endpoint "/api/pessoal/sindicatos/" Output:

  • Model: src/models/selects/select-sindicato.ts
  • Component: src/components/selects/select-sindicato.tsx
  • useModel: 'selects.select-sindicato'
  • Label: r.label

Example 2: Composite Label Input: Entity "colaborador", has codigo + nome fields Output:

  • Label mapping: ${r.codigo} - ${r.nome}
  • order_by: 'codigo'

Example 3: External Dependency Input: Entity depends on versaoRelatorioId Output:

  • Model accepts parameter: export default (versaoRelatorioId?: number) => {}
  • Component uses useUpdateEffect to reset on dependency change
Recommendation
Add more specific guidance on when NOT to use this skill - currently only mentions what entities to use it for
  • Each entity has its own dedicated model file
  • Use useModel('selects.select-<entity>') in components
  • Always implement infinite scroll for both default and search results
  • Include debounced search (700ms)
  • Support multiple, labelInValue, and isInstanceForColumnFilter props
  • Fill missing options automatically for pre-selected values
  • Clean up entity model after creating dedicated select model
  • Don't create generic shared select models
  • Don't forget to clean up the original entity model
  • Don't hardcode entity types - use proper TypeScript generics
  • Don't forget to update endpoint placeholder in search functionality
  • Don't skip the addMissedOptions implementation for pre-selected values
0
Grade B-AI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
12/15
Workflow
13/15
Examples
14/20
Completeness
10/20
Format
13/15
Conciseness
10/15