AI Skill Report Card
Generating Select Components
Generate Select Components
Quick Start15 / 15
Provide entity name in kebab-case (e.g., banco, centro-custo, sindicato) and I'll generate:
src/models/selects/select-<entity>.ts— Entity-specific modelsrc/components/selects/select-<entity>.tsx— Select component
Example:
Entity: sindicato
Controller: getSindicatosController
Endpoint: /api/pessoal/sindicatos/
Recommendation▾
Reduce verbosity in templates - they're quite long for a skill that assumes Claude is smart
Entity Identification
I'll check for:
- Attachments:
typings.d.tsor files with entity names - Editor context: Current file selection (e.g.,
Planejamento.Cadastro.Area) - User message: Entity name in request
If unclear, I'll ask for:
- Entity name (kebab-case)
- Controller function and import path
- API endpoint
- Label mapping (
r.labelor composite)
Model Template
TypeScriptimport { 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: '<order-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, }; 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'); } finally { stopLoading(); } }; return { selectLoading: loading, addMissedOptions, options, getOptions, getOptionsNewPage, queryStringOptionsParams, }; };
Component Template
TypeScriptimport { 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 debouncedSearch = useMemo( () => debounce((value: string) => { if (value) { setIsSearching(true); const params = { reset: true, params: { page: 1, page_size: 10, order_by: '<order-field>', search: value }, }; getSearchedOptions(params.reset, '<endpoint>', params.params); } else { setIsSearching(false); } }, 700), [], ); const optionsMemo = useMemo(() => { return (options.results || []).map((r: any) => ({ label: <label-mapping>, // r.label or `${r.codigo} - ${r.nome}` value: r.id, })); }, [options.results]); // ... search, scroll, and option filling logic return ( <Select searchValue={searchValue} onSearch={(value) => { setSearchValue(value); debouncedSearch(value); }} options={isSearching ? optionsSearched : optionsMemo} placeholder={props.placeholder || 'Selecione'} showSearch optionFilterProp="label" onPopupScroll={isSearching ? handleScrollSearched : handleScroll} loading={selectLoading || loading} value={props.value} onChange={props.onChange} mode={props.multiple ? 'multiple' : undefined} allowClear={props.allowClear} maxTagCount={3} disabled={props.disabled} labelInValue={props.labelInValue} {...columnFilterProps} /> ); };
Placeholder Substitutions
Model Placeholders:
get<Entity>Controller→ Real controller name (e.g.,getSindicatosController)<controller-path>→ Import path<order-field>→ Sort field ('label'or'codigo')EntityType→ TypeScript type from typings.d.ts
Component Placeholders:
'selects.select-<entity>'→ Model key (e.g.,'selects.select-sindicato')'<endpoint>'→ API endpoint (e.g.,'/api/pessoal/sindicatos/')<label-mapping>→ Simple:r.label| Composite:`${r.codigo} - ${r.nome}`
Examples18 / 20
Simple label:
Entity: sindicato
Model: src/models/selects/select-sindicato.ts
Component: src/components/selects/select-sindicato.tsx
Controller: getSindicatosController
Endpoint: '/api/pessoal/sindicatos/'
Order: 'label'
Label: r.label
Composite label:
Entity: colaborador
Controller: getColaboradoresEquipeController
Endpoint: '/api/pessoal/colaboradores/equipe/'
Order: 'codigo'
Label: `${r.codigo} - ${r.nome}`
Recommendation▾
The workflow section could be clearer about the step-by-step process rather than jumping between identification and templates
Best Practices
- Each entity has its own model and component
- Remove select logic from main entity models
- Use
addMissedOptionsfor pre-selected values - Implement infinite scroll and debounced search
- Support
labelInValuefor column filters - Handle dependencies with
useUpdateEffect
Common Pitfalls
- Don't share models between entities
- Don't duplicate select logic in entity models
- Always implement missing option filling
- Remember to replace all placeholders
- Test with pre-selected values and search functionality