AI Skill Report Card
Generating Select Components
Quick Start12 / 15
Provide the entity name in kebab-case (e.g., banco, centro-custo, sindicato) and I'll generate:
src/models/selects/select-<entity>.ts— dedicated model with states, loading, pagination, search, infinite scrollsrc/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
Workflow13 / 15
Step 1: Entity Identification
- Check attachments for
typings.d.tsor 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)
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: '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)
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 { 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
*Optionsstate (e.g.,contasContabeisOptions) - Remove
queryString*OptionsParamsstate - Remove
get*Options,get*OptionsNewPage,addMissed*Optionsfunctions - Remove
getUpdateSelectOptionsimport (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
Examples14 / 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
Best Practices
- 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, andisInstanceForColumnFilterprops - Fill missing options automatically for pre-selected values
- Clean up entity model after creating dedicated select model
Common Pitfalls
- 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