forlogic-core 1.7.4 → 1.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +571 -0
- package/dist/README.md +571 -0
- package/dist/index.esm.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/README.md
CHANGED
|
@@ -1634,6 +1634,577 @@ function ProcessLayout() {
|
|
|
1634
1634
|
|
|
1635
1635
|
---
|
|
1636
1636
|
|
|
1637
|
+
### **5️⃣ Ações Customizadas na Action Bar**
|
|
1638
|
+
|
|
1639
|
+
Adicione botões customizados ao lado do botão "Novo" na barra de ações.
|
|
1640
|
+
|
|
1641
|
+
#### **Casos de Uso Comuns:**
|
|
1642
|
+
- 📥 Importar dados
|
|
1643
|
+
- 📤 Exportar relatório
|
|
1644
|
+
- 🔄 Sincronizar com API externa
|
|
1645
|
+
- 📊 Abrir dashboard/visualizações
|
|
1646
|
+
|
|
1647
|
+
#### **Exemplo Básico:**
|
|
1648
|
+
|
|
1649
|
+
```typescript
|
|
1650
|
+
import { Download, Upload } from 'lucide-react';
|
|
1651
|
+
|
|
1652
|
+
const config = useMemo(() => ({
|
|
1653
|
+
entityName: "Processo",
|
|
1654
|
+
entityNamePlural: "Processos",
|
|
1655
|
+
columns: processColumns,
|
|
1656
|
+
formSections,
|
|
1657
|
+
|
|
1658
|
+
// ✅ Ações customizadas
|
|
1659
|
+
customActions: [
|
|
1660
|
+
{
|
|
1661
|
+
label: 'Importar',
|
|
1662
|
+
icon: Upload,
|
|
1663
|
+
variant: 'outline',
|
|
1664
|
+
action: () => {
|
|
1665
|
+
// Abrir dialog de importação
|
|
1666
|
+
setImportDialogOpen(true);
|
|
1667
|
+
}
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
label: 'Exportar',
|
|
1671
|
+
icon: Download,
|
|
1672
|
+
variant: 'outline',
|
|
1673
|
+
action: async () => {
|
|
1674
|
+
// Exportar dados filtrados/selecionados
|
|
1675
|
+
const data = manager.entities;
|
|
1676
|
+
await exportToCSV(data);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
]
|
|
1680
|
+
}), [manager.entities]);
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
#### **Propriedades:**
|
|
1684
|
+
|
|
1685
|
+
| Propriedade | Tipo | Obrigatório | Descrição |
|
|
1686
|
+
|------------|------|-------------|-----------|
|
|
1687
|
+
| `label` | `string` | ✅ | Texto do botão |
|
|
1688
|
+
| `icon` | `ComponentType` | ❌ | Ícone do lucide-react |
|
|
1689
|
+
| `variant` | `'default' \| 'destructive' \| 'outline' \| 'ghost'` | ❌ | Estilo do botão (default: `'outline'`) |
|
|
1690
|
+
| `action` | `() => void` | ✅ | Função executada ao clicar |
|
|
1691
|
+
|
|
1692
|
+
#### **Exemplo com Estado:**
|
|
1693
|
+
|
|
1694
|
+
```typescript
|
|
1695
|
+
export default function ProcessesPage() {
|
|
1696
|
+
const manager = useProcesses();
|
|
1697
|
+
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
|
1698
|
+
const [syncLoading, setSyncLoading] = useState(false);
|
|
1699
|
+
|
|
1700
|
+
const handleSyncWithAPI = async () => {
|
|
1701
|
+
setSyncLoading(true);
|
|
1702
|
+
try {
|
|
1703
|
+
await syncProcessesWithExternalAPI();
|
|
1704
|
+
manager.refetch(); // Recarregar dados
|
|
1705
|
+
toast({ title: 'Sincronização concluída' });
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
toast({ title: 'Erro na sincronização', variant: 'destructive' });
|
|
1708
|
+
} finally {
|
|
1709
|
+
setSyncLoading(false);
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
const config = useMemo(() => ({
|
|
1714
|
+
entityName: "Processo",
|
|
1715
|
+
entityNamePlural: "Processos",
|
|
1716
|
+
columns: processColumns,
|
|
1717
|
+
formSections,
|
|
1718
|
+
customActions: [
|
|
1719
|
+
{
|
|
1720
|
+
label: syncLoading ? 'Sincronizando...' : 'Sincronizar',
|
|
1721
|
+
icon: RefreshCw,
|
|
1722
|
+
variant: 'outline',
|
|
1723
|
+
action: handleSyncWithAPI
|
|
1724
|
+
}
|
|
1725
|
+
]
|
|
1726
|
+
}), [syncLoading, handleSyncWithAPI]);
|
|
1727
|
+
|
|
1728
|
+
return (
|
|
1729
|
+
<>
|
|
1730
|
+
<CrudPage />
|
|
1731
|
+
<ImportDialog open={importDialogOpen} onOpenChange={setImportDialogOpen} />
|
|
1732
|
+
</>
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
#### **Integração com Bulk Actions:**
|
|
1738
|
+
|
|
1739
|
+
```typescript
|
|
1740
|
+
customActions: [
|
|
1741
|
+
{
|
|
1742
|
+
label: 'Exportar Selecionados',
|
|
1743
|
+
icon: Download,
|
|
1744
|
+
variant: 'outline',
|
|
1745
|
+
action: () => {
|
|
1746
|
+
if (manager.selectedIds.length === 0) {
|
|
1747
|
+
toast({ title: 'Selecione pelo menos um item', variant: 'destructive' });
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const selectedItems = manager.entities.filter(
|
|
1752
|
+
item => manager.selectedIds.includes(item.id)
|
|
1753
|
+
);
|
|
1754
|
+
exportToCSV(selectedItems);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
]
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
**Boas Práticas:**
|
|
1761
|
+
- ✅ Use `useMemo` para evitar recriação em cada render
|
|
1762
|
+
- ✅ Use `variant="outline"` para ações secundárias
|
|
1763
|
+
- ✅ Use ícones do `lucide-react` para consistência visual
|
|
1764
|
+
- ✅ Mostre feedback (loading, toasts) para ações assíncronas
|
|
1765
|
+
- ❌ Evite muitas ações (máximo 3-4 para não poluir a UI)
|
|
1766
|
+
|
|
1767
|
+
---
|
|
1768
|
+
|
|
1769
|
+
### **6️⃣ Views Customizadas (substituir Tabela/Cards)**
|
|
1770
|
+
|
|
1771
|
+
Substitua completamente a tabela padrão por qualquer componente customizado.
|
|
1772
|
+
|
|
1773
|
+
#### **Casos de Uso Comuns:**
|
|
1774
|
+
- 📅 Calendário de eventos
|
|
1775
|
+
- 🗺️ Mapa de localizações
|
|
1776
|
+
- 🎨 Galeria de imagens
|
|
1777
|
+
- 📊 Dashboard com widgets
|
|
1778
|
+
- 🌳 Árvore hierárquica (processos/subprocessos)
|
|
1779
|
+
- 📋 Kanban board
|
|
1780
|
+
|
|
1781
|
+
#### **Exemplo: Kanban Board**
|
|
1782
|
+
|
|
1783
|
+
```typescript
|
|
1784
|
+
import { useMemo } from 'react';
|
|
1785
|
+
import { DragDropContext, Droppable, Draggable } from '@dnd-kit/core';
|
|
1786
|
+
|
|
1787
|
+
const KanbanColumn = ({ title, tasks, status }) => (
|
|
1788
|
+
<div className="flex flex-col gap-2 p-4 bg-muted rounded-lg min-w-[300px]">
|
|
1789
|
+
<h3 className="font-semibold text-sm text-muted-foreground uppercase">{title}</h3>
|
|
1790
|
+
<Droppable droppableId={status}>
|
|
1791
|
+
{tasks.map((task, index) => (
|
|
1792
|
+
<Draggable key={task.id} draggableId={task.id} index={index}>
|
|
1793
|
+
<div className="p-3 bg-background border rounded shadow-sm cursor-move">
|
|
1794
|
+
<p className="font-medium">{task.title}</p>
|
|
1795
|
+
<p className="text-xs text-muted-foreground mt-1">{task.description}</p>
|
|
1796
|
+
</div>
|
|
1797
|
+
</Draggable>
|
|
1798
|
+
))}
|
|
1799
|
+
</Droppable>
|
|
1800
|
+
</div>
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1803
|
+
export default function TasksPage() {
|
|
1804
|
+
const manager = useTasks();
|
|
1805
|
+
|
|
1806
|
+
const config = useMemo(() => ({
|
|
1807
|
+
entityName: "Tarefa",
|
|
1808
|
+
entityNamePlural: "Tarefas",
|
|
1809
|
+
columns: taskColumns,
|
|
1810
|
+
formSections,
|
|
1811
|
+
|
|
1812
|
+
// ✅ View customizada (Kanban)
|
|
1813
|
+
customListView: (tasks: Task[], manager: CrudManager<Task>) => {
|
|
1814
|
+
const columns = {
|
|
1815
|
+
todo: tasks.filter(t => t.status === 'todo'),
|
|
1816
|
+
in_progress: tasks.filter(t => t.status === 'in_progress'),
|
|
1817
|
+
done: tasks.filter(t => t.status === 'done')
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
return (
|
|
1821
|
+
<DragDropContext onDragEnd={handleDragEnd}>
|
|
1822
|
+
<div className="flex gap-4 overflow-x-auto">
|
|
1823
|
+
<KanbanColumn title="A Fazer" tasks={columns.todo} status="todo" />
|
|
1824
|
+
<KanbanColumn title="Em Progresso" tasks={columns.in_progress} status="in_progress" />
|
|
1825
|
+
<KanbanColumn title="Concluído" tasks={columns.done} status="done" />
|
|
1826
|
+
</div>
|
|
1827
|
+
</DragDropContext>
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
}), [manager.entities]);
|
|
1831
|
+
|
|
1832
|
+
return <CrudPage />;
|
|
1833
|
+
}
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
#### **Exemplo: Calendário de Eventos**
|
|
1837
|
+
|
|
1838
|
+
```typescript
|
|
1839
|
+
import { Calendar } from '@/components/ui/calendar';
|
|
1840
|
+
import { Badge } from 'forlogic-core';
|
|
1841
|
+
import { format } from 'date-fns';
|
|
1842
|
+
import { ptBR } from 'date-fns/locale';
|
|
1843
|
+
|
|
1844
|
+
const config = useMemo(() => ({
|
|
1845
|
+
entityName: "Evento",
|
|
1846
|
+
entityNamePlural: "Eventos",
|
|
1847
|
+
columns: eventColumns,
|
|
1848
|
+
formSections,
|
|
1849
|
+
|
|
1850
|
+
customListView: (events: Event[]) => {
|
|
1851
|
+
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
|
1852
|
+
|
|
1853
|
+
// Agrupar eventos por data
|
|
1854
|
+
const eventsByDate = events.reduce((acc, event) => {
|
|
1855
|
+
const dateKey = format(new Date(event.date), 'yyyy-MM-dd');
|
|
1856
|
+
if (!acc[dateKey]) acc[dateKey] = [];
|
|
1857
|
+
acc[dateKey].push(event);
|
|
1858
|
+
return acc;
|
|
1859
|
+
}, {} as Record<string, Event[]>);
|
|
1860
|
+
|
|
1861
|
+
const selectedDateKey = format(selectedDate, 'yyyy-MM-dd');
|
|
1862
|
+
const eventsForSelectedDate = eventsByDate[selectedDateKey] || [];
|
|
1863
|
+
|
|
1864
|
+
return (
|
|
1865
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
1866
|
+
{/* Calendário */}
|
|
1867
|
+
<div>
|
|
1868
|
+
<Calendar
|
|
1869
|
+
mode="single"
|
|
1870
|
+
selected={selectedDate}
|
|
1871
|
+
onSelect={(date) => date && setSelectedDate(date)}
|
|
1872
|
+
locale={ptBR}
|
|
1873
|
+
modifiers={{
|
|
1874
|
+
hasEvents: Object.keys(eventsByDate).map(d => new Date(d))
|
|
1875
|
+
}}
|
|
1876
|
+
modifiersStyles={{
|
|
1877
|
+
hasEvents: { fontWeight: 'bold', textDecoration: 'underline' }
|
|
1878
|
+
}}
|
|
1879
|
+
/>
|
|
1880
|
+
</div>
|
|
1881
|
+
|
|
1882
|
+
{/* Lista de eventos do dia selecionado */}
|
|
1883
|
+
<div>
|
|
1884
|
+
<h3 className="font-semibold mb-4">
|
|
1885
|
+
Eventos de {format(selectedDate, "dd 'de' MMMM", { locale: ptBR })}
|
|
1886
|
+
</h3>
|
|
1887
|
+
|
|
1888
|
+
{eventsForSelectedDate.length === 0 ? (
|
|
1889
|
+
<p className="text-muted-foreground">Nenhum evento neste dia</p>
|
|
1890
|
+
) : (
|
|
1891
|
+
<div className="space-y-3">
|
|
1892
|
+
{eventsForSelectedDate.map(event => (
|
|
1893
|
+
<div key={event.id} className="p-4 border rounded-lg">
|
|
1894
|
+
<div className="flex items-start justify-between">
|
|
1895
|
+
<div>
|
|
1896
|
+
<p className="font-medium">{event.title}</p>
|
|
1897
|
+
<p className="text-sm text-muted-foreground">{event.time}</p>
|
|
1898
|
+
</div>
|
|
1899
|
+
<Badge>{event.type}</Badge>
|
|
1900
|
+
</div>
|
|
1901
|
+
<p className="text-sm mt-2">{event.description}</p>
|
|
1902
|
+
</div>
|
|
1903
|
+
))}
|
|
1904
|
+
</div>
|
|
1905
|
+
)}
|
|
1906
|
+
</div>
|
|
1907
|
+
</div>
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
}), [manager.entities]);
|
|
1911
|
+
```
|
|
1912
|
+
|
|
1913
|
+
#### **Exemplo: Galeria de Imagens**
|
|
1914
|
+
|
|
1915
|
+
```typescript
|
|
1916
|
+
import { Dialog, DialogContent, DialogTrigger } from 'forlogic-core';
|
|
1917
|
+
|
|
1918
|
+
const config = useMemo(() => ({
|
|
1919
|
+
entityName: "Foto",
|
|
1920
|
+
entityNamePlural: "Fotos",
|
|
1921
|
+
columns: photoColumns,
|
|
1922
|
+
formSections,
|
|
1923
|
+
|
|
1924
|
+
customListView: (photos: Photo[]) => (
|
|
1925
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
1926
|
+
{photos.map(photo => (
|
|
1927
|
+
<Dialog key={photo.id}>
|
|
1928
|
+
<DialogTrigger asChild>
|
|
1929
|
+
<div className="aspect-square relative rounded-lg overflow-hidden cursor-pointer group">
|
|
1930
|
+
<img
|
|
1931
|
+
src={photo.thumbnail_url}
|
|
1932
|
+
alt={photo.title}
|
|
1933
|
+
className="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
|
1934
|
+
/>
|
|
1935
|
+
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
1936
|
+
<p className="text-white font-medium">{photo.title}</p>
|
|
1937
|
+
</div>
|
|
1938
|
+
</div>
|
|
1939
|
+
</DialogTrigger>
|
|
1940
|
+
|
|
1941
|
+
<DialogContent className="max-w-4xl">
|
|
1942
|
+
<img src={photo.full_url} alt={photo.title} className="w-full" />
|
|
1943
|
+
<div className="mt-4">
|
|
1944
|
+
<h3 className="font-semibold text-lg">{photo.title}</h3>
|
|
1945
|
+
<p className="text-muted-foreground">{photo.description}</p>
|
|
1946
|
+
</div>
|
|
1947
|
+
</DialogContent>
|
|
1948
|
+
</Dialog>
|
|
1949
|
+
))}
|
|
1950
|
+
</div>
|
|
1951
|
+
)
|
|
1952
|
+
}), [manager.entities]);
|
|
1953
|
+
```
|
|
1954
|
+
|
|
1955
|
+
#### **Propriedades Recebidas:**
|
|
1956
|
+
|
|
1957
|
+
```typescript
|
|
1958
|
+
customListView: (items: T[], manager?: CrudManager<T>) => React.ReactNode
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
| Parâmetro | Tipo | Descrição |
|
|
1962
|
+
|-----------|------|-----------|
|
|
1963
|
+
| `items` | `T[]` | Entidades filtradas/paginadas do manager |
|
|
1964
|
+
| `manager` | `CrudManager<T>` | Manager completo com métodos de CRUD |
|
|
1965
|
+
|
|
1966
|
+
**Acesso ao Manager:**
|
|
1967
|
+
```typescript
|
|
1968
|
+
customListView: (items, manager) => (
|
|
1969
|
+
<div>
|
|
1970
|
+
{/* Acessar estado de loading */}
|
|
1971
|
+
{manager.isLoading && <Spinner />}
|
|
1972
|
+
|
|
1973
|
+
{/* Acessar busca atual */}
|
|
1974
|
+
<p>Busca: {manager.searchTerm}</p>
|
|
1975
|
+
|
|
1976
|
+
{/* Refetch manual */}
|
|
1977
|
+
<Button onClick={() => manager.refetch()}>Recarregar</Button>
|
|
1978
|
+
|
|
1979
|
+
{/* Criar nova entidade */}
|
|
1980
|
+
<Button onClick={() => manager.createEntity(newData)}>Criar</Button>
|
|
1981
|
+
</div>
|
|
1982
|
+
)
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
#### **Comportamento:**
|
|
1986
|
+
|
|
1987
|
+
✅ **Mantém todas as funcionalidades:**
|
|
1988
|
+
- Paginação (footer fixo)
|
|
1989
|
+
- Busca global
|
|
1990
|
+
- Filtros customizados
|
|
1991
|
+
- Bulk actions (se habilitado)
|
|
1992
|
+
- Botão "Novo" + Custom Actions
|
|
1993
|
+
|
|
1994
|
+
❌ **Remove automaticamente:**
|
|
1995
|
+
- Tabela/Cards padrão
|
|
1996
|
+
- Ações de linha (editar/deletar inline)
|
|
1997
|
+
- Seleção de itens (checkbox)
|
|
1998
|
+
|
|
1999
|
+
**💡 Dica:** Se precisar de ações inline, implemente no seu componente customizado:
|
|
2000
|
+
|
|
2001
|
+
```typescript
|
|
2002
|
+
customListView: (items, manager) => (
|
|
2003
|
+
<div className="space-y-2">
|
|
2004
|
+
{items.map(item => (
|
|
2005
|
+
<div key={item.id} className="flex justify-between items-center p-4 border rounded">
|
|
2006
|
+
<span>{item.title}</span>
|
|
2007
|
+
<div className="flex gap-2">
|
|
2008
|
+
<Button size="sm" onClick={() => handleEdit(item)}>Editar</Button>
|
|
2009
|
+
<Button size="sm" variant="destructive" onClick={() => manager.deleteEntity(item.id)}>
|
|
2010
|
+
Deletar
|
|
2011
|
+
</Button>
|
|
2012
|
+
</div>
|
|
2013
|
+
</div>
|
|
2014
|
+
))}
|
|
2015
|
+
</div>
|
|
2016
|
+
)
|
|
2017
|
+
```
|
|
2018
|
+
|
|
2019
|
+
**Boas Práticas:**
|
|
2020
|
+
- ✅ Use `useMemo` para evitar recriação em cada render
|
|
2021
|
+
- ✅ Mantenha responsividade (grid com breakpoints)
|
|
2022
|
+
- ✅ Implemente loading states se necessário
|
|
2023
|
+
- ✅ Use componentes da lib (`Dialog`, `Badge`, etc) para consistência
|
|
2024
|
+
- ❌ Evite lógica pesada no render (mova para useMemo/useCallback)
|
|
2025
|
+
|
|
2026
|
+
---
|
|
2027
|
+
|
|
2028
|
+
### **7️⃣ Customizar Comportamento de Edição (`onEdit`)**
|
|
2029
|
+
|
|
2030
|
+
Sobrescreva o comportamento padrão ao clicar em "Editar" nas linhas.
|
|
2031
|
+
|
|
2032
|
+
#### **Casos de Uso Comuns:**
|
|
2033
|
+
- 🔀 Navegação para página dedicada de edição
|
|
2034
|
+
- 📋 Abrir modal customizado (diferente do BaseForm)
|
|
2035
|
+
- 🔍 Abrir modal somente-leitura (resumo)
|
|
2036
|
+
- 🔄 Transformar dados antes de abrir o formulário
|
|
2037
|
+
|
|
2038
|
+
#### **Comportamento Padrão (sem `onEdit`):**
|
|
2039
|
+
|
|
2040
|
+
```typescript
|
|
2041
|
+
// Ao clicar em "Editar":
|
|
2042
|
+
// 1. Abre modal com BaseForm
|
|
2043
|
+
// 2. Preenche campos com dados da entidade
|
|
2044
|
+
// 3. Chama handleSave ao submeter
|
|
2045
|
+
```
|
|
2046
|
+
|
|
2047
|
+
#### **Exemplo 1: Navegação para Página Dedicada**
|
|
2048
|
+
|
|
2049
|
+
```typescript
|
|
2050
|
+
import { useNavigate } from 'react-router-dom';
|
|
2051
|
+
|
|
2052
|
+
export default function ProcessesPage() {
|
|
2053
|
+
const navigate = useNavigate();
|
|
2054
|
+
const manager = useProcesses();
|
|
2055
|
+
|
|
2056
|
+
const config = useMemo(() => ({
|
|
2057
|
+
entityName: "Processo",
|
|
2058
|
+
entityNamePlural: "Processos",
|
|
2059
|
+
columns: processColumns,
|
|
2060
|
+
formSections,
|
|
2061
|
+
|
|
2062
|
+
// ✅ Navegar ao invés de abrir modal
|
|
2063
|
+
onEdit: (process: Process) => {
|
|
2064
|
+
navigate(`/processos/${process.id}/editar`);
|
|
2065
|
+
// Retornar void = não abre modal
|
|
2066
|
+
}
|
|
2067
|
+
}), [navigate]);
|
|
2068
|
+
|
|
2069
|
+
return <CrudPage />;
|
|
2070
|
+
}
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
#### **Exemplo 2: Modal Somente-Leitura para Status Específico**
|
|
2074
|
+
|
|
2075
|
+
```typescript
|
|
2076
|
+
// TrainingRequestsPage.tsx
|
|
2077
|
+
export default function TrainingRequestsPage() {
|
|
2078
|
+
const manager = useTrainingRequests();
|
|
2079
|
+
const [summaryModal, setSummaryModal] = useState<{
|
|
2080
|
+
isOpen: boolean;
|
|
2081
|
+
request: TrainingRequest | null;
|
|
2082
|
+
}>({ isOpen: false, request: null });
|
|
2083
|
+
|
|
2084
|
+
const config = useMemo(() => ({
|
|
2085
|
+
entityName: "Solicitação",
|
|
2086
|
+
entityNamePlural: "Solicitações",
|
|
2087
|
+
columns,
|
|
2088
|
+
formSections,
|
|
2089
|
+
|
|
2090
|
+
// ✅ Modal de resumo se aprovada, editor padrão se não
|
|
2091
|
+
onEdit: (request: TrainingRequest) => {
|
|
2092
|
+
if (request.status === 'approved') {
|
|
2093
|
+
// Abrir modal customizado
|
|
2094
|
+
setSummaryModal({ isOpen: true, request });
|
|
2095
|
+
return; // void = não abre BaseForm
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
// Retornar undefined = usar comportamento padrão (abrir BaseForm)
|
|
2099
|
+
// Nenhum return explícito necessário
|
|
2100
|
+
}
|
|
2101
|
+
}), []);
|
|
2102
|
+
|
|
2103
|
+
return (
|
|
2104
|
+
<>
|
|
2105
|
+
<CrudPage />
|
|
2106
|
+
|
|
2107
|
+
{/* Modal customizado */}
|
|
2108
|
+
<TrainingRequestSummaryModal
|
|
2109
|
+
request={summaryModal.request}
|
|
2110
|
+
isOpen={summaryModal.isOpen}
|
|
2111
|
+
onClose={() => setSummaryModal({ isOpen: false, request: null })}
|
|
2112
|
+
/>
|
|
2113
|
+
</>
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
```
|
|
2117
|
+
|
|
2118
|
+
#### **Exemplo 3: Transformar Dados Antes de Editar**
|
|
2119
|
+
|
|
2120
|
+
```typescript
|
|
2121
|
+
const config = useMemo(() => ({
|
|
2122
|
+
entityName: "Produto",
|
|
2123
|
+
entityNamePlural: "Produtos",
|
|
2124
|
+
columns,
|
|
2125
|
+
formSections,
|
|
2126
|
+
|
|
2127
|
+
// ✅ Modificar dados antes de popular o formulário
|
|
2128
|
+
onEdit: (product: Product) => {
|
|
2129
|
+
// Transformar preço de centavos para reais
|
|
2130
|
+
const transformedProduct = {
|
|
2131
|
+
...product,
|
|
2132
|
+
price: product.price / 100, // 1999 → 19.99
|
|
2133
|
+
categories: product.category_ids?.join(',') // [1,2] → "1,2"
|
|
2134
|
+
};
|
|
2135
|
+
|
|
2136
|
+
// Retornar entidade transformada = abre BaseForm com dados modificados
|
|
2137
|
+
return transformedProduct;
|
|
2138
|
+
}
|
|
2139
|
+
}), []);
|
|
2140
|
+
```
|
|
2141
|
+
|
|
2142
|
+
#### **Assinatura:**
|
|
2143
|
+
|
|
2144
|
+
```typescript
|
|
2145
|
+
onEdit?: (entity: T) => T | void
|
|
2146
|
+
```
|
|
2147
|
+
|
|
2148
|
+
| Retorno | Comportamento |
|
|
2149
|
+
|---------|---------------|
|
|
2150
|
+
| `T` (entidade transformada) | Abre `BaseForm` com dados transformados |
|
|
2151
|
+
| `void` / `undefined` | Não abre `BaseForm` (comportamento totalmente customizado) |
|
|
2152
|
+
| Sem `onEdit` | Comportamento padrão (abre `BaseForm` com dados originais) |
|
|
2153
|
+
|
|
2154
|
+
#### **Exemplo 4: Condicionalmente Bloquear Edição**
|
|
2155
|
+
|
|
2156
|
+
```typescript
|
|
2157
|
+
import { toast } from 'forlogic-core';
|
|
2158
|
+
|
|
2159
|
+
const config = useMemo(() => ({
|
|
2160
|
+
entityName: "Documento",
|
|
2161
|
+
entityNamePlural: "Documentos",
|
|
2162
|
+
columns,
|
|
2163
|
+
formSections,
|
|
2164
|
+
|
|
2165
|
+
onEdit: (document: Document) => {
|
|
2166
|
+
// Bloquear edição se documento arquivado
|
|
2167
|
+
if (document.status === 'archived') {
|
|
2168
|
+
toast({
|
|
2169
|
+
title: 'Documento arquivado',
|
|
2170
|
+
description: 'Não é possível editar documentos arquivados',
|
|
2171
|
+
variant: 'destructive'
|
|
2172
|
+
});
|
|
2173
|
+
return; // void = não abre modal
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// Permitir edição normalmente (sem return = undefined = padrão)
|
|
2177
|
+
}
|
|
2178
|
+
}), []);
|
|
2179
|
+
```
|
|
2180
|
+
|
|
2181
|
+
**Boas Práticas:**
|
|
2182
|
+
- ✅ Use `onEdit` para casos específicos (não generalize demais)
|
|
2183
|
+
- ✅ Retorne `void` quando **não quer** abrir o `BaseForm`
|
|
2184
|
+
- ✅ Retorne entidade transformada quando **quer** abrir o `BaseForm` com dados modificados
|
|
2185
|
+
- ✅ Não retorne nada (undefined) para usar comportamento padrão
|
|
2186
|
+
- ❌ Evite lógica pesada no `onEdit` (mova para funções separadas)
|
|
2187
|
+
|
|
2188
|
+
**Integração com `useCustomRouting`:**
|
|
2189
|
+
|
|
2190
|
+
```typescript
|
|
2191
|
+
const config = useMemo(() => ({
|
|
2192
|
+
entityName: "Artigo",
|
|
2193
|
+
entityNamePlural: "Artigos",
|
|
2194
|
+
columns,
|
|
2195
|
+
formSections,
|
|
2196
|
+
useCustomRouting: true, // ✅ Não renderiza BaseForm
|
|
2197
|
+
onEdit: (article) => {
|
|
2198
|
+
navigate(`/artigos/${article.id}`);
|
|
2199
|
+
},
|
|
2200
|
+
onNew: () => {
|
|
2201
|
+
navigate('/artigos/novo');
|
|
2202
|
+
}
|
|
2203
|
+
}), [navigate]);
|
|
2204
|
+
```
|
|
2205
|
+
|
|
2206
|
+
---
|
|
2207
|
+
|
|
1637
2208
|
## 🔄 GUIA DE MIGRAÇÃO - v2.0
|
|
1638
2209
|
|
|
1639
2210
|
Se você tem projetos usando versões antigas do `forlogic-core`, siga este guia para atualizar.
|