@greatapps/greatagents-ui 0.1.0 → 0.2.1
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/dist/index.d.ts +178 -1
- package/dist/index.js +3378 -0
- package/dist/index.js.map +1 -1
- package/package.json +13 -3
- package/src/components/agents/agent-edit-form.tsx +218 -0
- package/src/components/agents/agent-form-dialog.tsx +177 -0
- package/src/components/agents/agent-objectives-list.tsx +406 -0
- package/src/components/agents/agent-prompt-editor.tsx +406 -0
- package/src/components/agents/agent-tabs.tsx +64 -0
- package/src/components/agents/agent-tools-list.tsx +377 -0
- package/src/components/agents/agents-table.tsx +205 -0
- package/src/components/conversations/agent-conversations-panel.tsx +44 -0
- package/src/components/conversations/agent-conversations-table.tsx +160 -0
- package/src/components/conversations/conversation-view.tsx +124 -0
- package/src/components/tools/tool-credentials-form.tsx +572 -0
- package/src/components/tools/tool-form-dialog.tsx +342 -0
- package/src/components/tools/tools-table.tsx +225 -0
- package/src/components/ui/sortable.tsx +577 -0
- package/src/index.ts +19 -0
- package/src/lib/compose-refs.ts +44 -0
- package/src/pages/agent-detail-page.tsx +111 -0
- package/src/pages/agents-page.tsx +45 -0
- package/src/pages/credentials-page.tsx +50 -0
- package/src/pages/index.ts +4 -0
- package/src/pages/tools-page.tsx +52 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useCreateTool, useUpdateTool } from "../../hooks";
|
|
3
|
+
import type { Tool } from "../../types";
|
|
4
|
+
import type { GagentsHookConfig } from "../../hooks/types";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
Button,
|
|
12
|
+
Input,
|
|
13
|
+
Textarea,
|
|
14
|
+
Label,
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
21
|
+
import { Loader2 } from "lucide-react";
|
|
22
|
+
import { toast } from "sonner";
|
|
23
|
+
|
|
24
|
+
const TOOL_AUTH_TYPES = [
|
|
25
|
+
{ value: "none", label: "Nenhuma" },
|
|
26
|
+
{ value: "api_key", label: "API Key" },
|
|
27
|
+
{ value: "oauth2", label: "OAuth 2.0" },
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
interface ToolFormDialogProps {
|
|
31
|
+
open: boolean;
|
|
32
|
+
onOpenChange: (open: boolean) => void;
|
|
33
|
+
tool?: Tool;
|
|
34
|
+
config: GagentsHookConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FormState {
|
|
38
|
+
name: string;
|
|
39
|
+
slug: string;
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
functionDefinitions: string;
|
|
43
|
+
nameError: boolean;
|
|
44
|
+
slugError: boolean;
|
|
45
|
+
typeError: boolean;
|
|
46
|
+
jsonError: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toolToFormState(tool?: Tool): FormState {
|
|
50
|
+
return {
|
|
51
|
+
name: tool?.name || "",
|
|
52
|
+
slug: tool?.slug || "",
|
|
53
|
+
type: tool?.type || "none",
|
|
54
|
+
description: tool?.description || "",
|
|
55
|
+
functionDefinitions: tool?.function_definitions
|
|
56
|
+
? formatJson(tool.function_definitions)
|
|
57
|
+
: "",
|
|
58
|
+
nameError: false,
|
|
59
|
+
slugError: false,
|
|
60
|
+
typeError: false,
|
|
61
|
+
jsonError: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatJson(str: string): string {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.stringify(JSON.parse(str), null, 2);
|
|
68
|
+
} catch {
|
|
69
|
+
return str;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function slugify(name: string): string {
|
|
74
|
+
return name
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.normalize("NFD")
|
|
77
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
78
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
79
|
+
.replace(/^-+|-+$/g, "");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isValidJson(str: string): boolean {
|
|
83
|
+
if (!str.trim()) return true;
|
|
84
|
+
try {
|
|
85
|
+
JSON.parse(str);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function ToolFormDialog({
|
|
93
|
+
open,
|
|
94
|
+
onOpenChange,
|
|
95
|
+
tool,
|
|
96
|
+
config,
|
|
97
|
+
}: ToolFormDialogProps) {
|
|
98
|
+
const isEditing = !!tool;
|
|
99
|
+
const createTool = useCreateTool(config);
|
|
100
|
+
const updateTool = useUpdateTool(config);
|
|
101
|
+
const [form, setForm] = useState<FormState>(() => toolToFormState(tool));
|
|
102
|
+
const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);
|
|
103
|
+
|
|
104
|
+
// Reset form when dialog opens or tool changes
|
|
105
|
+
const [lastResetKey, setLastResetKey] = useState(
|
|
106
|
+
() => `${tool?.id}-${open}`,
|
|
107
|
+
);
|
|
108
|
+
const resetKey = `${tool?.id}-${open}`;
|
|
109
|
+
if (resetKey !== lastResetKey) {
|
|
110
|
+
setLastResetKey(resetKey);
|
|
111
|
+
setForm(toolToFormState(open ? tool : undefined));
|
|
112
|
+
setSlugManuallyEdited(false);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isPending = createTool.isPending || updateTool.isPending;
|
|
116
|
+
|
|
117
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
|
|
120
|
+
let hasError = false;
|
|
121
|
+
|
|
122
|
+
if (!form.name.trim()) {
|
|
123
|
+
setForm((prev) => ({ ...prev, nameError: true }));
|
|
124
|
+
hasError = true;
|
|
125
|
+
}
|
|
126
|
+
const effectiveSlug = form.slug.trim() || slugify(form.name);
|
|
127
|
+
if (!effectiveSlug) {
|
|
128
|
+
setForm((prev) => ({ ...prev, slugError: true }));
|
|
129
|
+
hasError = true;
|
|
130
|
+
}
|
|
131
|
+
if (!form.type) {
|
|
132
|
+
setForm((prev) => ({ ...prev, typeError: true }));
|
|
133
|
+
hasError = true;
|
|
134
|
+
}
|
|
135
|
+
if (!isValidJson(form.functionDefinitions)) {
|
|
136
|
+
setForm((prev) => ({ ...prev, jsonError: true }));
|
|
137
|
+
hasError = true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hasError) return;
|
|
141
|
+
|
|
142
|
+
const body: Record<string, unknown> = {
|
|
143
|
+
name: form.name.trim(),
|
|
144
|
+
slug: effectiveSlug,
|
|
145
|
+
type: form.type,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (form.description.trim()) body.description = form.description.trim();
|
|
149
|
+
else body.description = "";
|
|
150
|
+
|
|
151
|
+
// JSONB critical rule: function_definitions must be sent as string
|
|
152
|
+
if (form.functionDefinitions.trim()) {
|
|
153
|
+
const parsed = JSON.parse(form.functionDefinitions.trim());
|
|
154
|
+
body.function_definitions = JSON.stringify(parsed);
|
|
155
|
+
} else {
|
|
156
|
+
body.function_definitions = "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
if (isEditing) {
|
|
161
|
+
await updateTool.mutateAsync({ id: tool.id, body });
|
|
162
|
+
toast.success("Ferramenta atualizada");
|
|
163
|
+
} else {
|
|
164
|
+
await createTool.mutateAsync(
|
|
165
|
+
body as { name: string; slug: string; type: string; description?: string; function_definitions?: string },
|
|
166
|
+
);
|
|
167
|
+
toast.success("Ferramenta criada");
|
|
168
|
+
}
|
|
169
|
+
onOpenChange(false);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
toast.error(
|
|
172
|
+
err instanceof Error
|
|
173
|
+
? err.message
|
|
174
|
+
: isEditing ? "Erro ao atualizar ferramenta" : "Erro ao criar ferramenta",
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
181
|
+
<DialogContent className="sm:max-w-lg">
|
|
182
|
+
<DialogHeader>
|
|
183
|
+
<DialogTitle>
|
|
184
|
+
{isEditing ? "Editar Ferramenta" : "Nova Ferramenta"}
|
|
185
|
+
</DialogTitle>
|
|
186
|
+
</DialogHeader>
|
|
187
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
188
|
+
<div className="space-y-2">
|
|
189
|
+
<Label htmlFor="tool-name">Nome *</Label>
|
|
190
|
+
<Input
|
|
191
|
+
id="tool-name"
|
|
192
|
+
value={form.name}
|
|
193
|
+
onChange={(e) => {
|
|
194
|
+
const name = e.target.value;
|
|
195
|
+
setForm((prev) => ({
|
|
196
|
+
...prev,
|
|
197
|
+
name,
|
|
198
|
+
nameError: name.trim() ? false : prev.nameError,
|
|
199
|
+
...(!slugManuallyEdited && !isEditing
|
|
200
|
+
? { slug: slugify(name), slugError: false }
|
|
201
|
+
: {}),
|
|
202
|
+
}));
|
|
203
|
+
}}
|
|
204
|
+
placeholder="Ex: Google Calendar"
|
|
205
|
+
disabled={isPending}
|
|
206
|
+
/>
|
|
207
|
+
{form.nameError && (
|
|
208
|
+
<p className="text-sm text-destructive">Nome é obrigatório</p>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="space-y-2">
|
|
213
|
+
<Label htmlFor="tool-slug">Slug (identificador único) *</Label>
|
|
214
|
+
<Input
|
|
215
|
+
id="tool-slug"
|
|
216
|
+
value={form.slug}
|
|
217
|
+
onChange={(e) => {
|
|
218
|
+
setSlugManuallyEdited(true);
|
|
219
|
+
setForm((prev) => ({
|
|
220
|
+
...prev,
|
|
221
|
+
slug: e.target.value,
|
|
222
|
+
slugError: e.target.value.trim() ? false : prev.slugError,
|
|
223
|
+
}));
|
|
224
|
+
}}
|
|
225
|
+
placeholder="Ex: google-calendar"
|
|
226
|
+
disabled={isPending}
|
|
227
|
+
/>
|
|
228
|
+
<p className="text-xs text-muted-foreground">
|
|
229
|
+
Gerado automaticamente a partir do nome. Usado internamente para identificar a ferramenta.
|
|
230
|
+
</p>
|
|
231
|
+
{form.slugError && (
|
|
232
|
+
<p className="text-sm text-destructive">Slug é obrigatório</p>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div className="space-y-2">
|
|
237
|
+
<Label htmlFor="tool-type">Tipo de Autenticação *</Label>
|
|
238
|
+
<Select
|
|
239
|
+
value={form.type}
|
|
240
|
+
onValueChange={(value) => {
|
|
241
|
+
setForm((prev) => ({
|
|
242
|
+
...prev,
|
|
243
|
+
type: value,
|
|
244
|
+
typeError: false,
|
|
245
|
+
}));
|
|
246
|
+
}}
|
|
247
|
+
disabled={isPending}
|
|
248
|
+
>
|
|
249
|
+
<SelectTrigger id="tool-type">
|
|
250
|
+
<SelectValue placeholder="Selecione o tipo" />
|
|
251
|
+
</SelectTrigger>
|
|
252
|
+
<SelectContent>
|
|
253
|
+
{TOOL_AUTH_TYPES.map((t) => (
|
|
254
|
+
<SelectItem key={t.value} value={t.value}>
|
|
255
|
+
{t.label}
|
|
256
|
+
</SelectItem>
|
|
257
|
+
))}
|
|
258
|
+
</SelectContent>
|
|
259
|
+
</Select>
|
|
260
|
+
<p className="text-xs text-muted-foreground">
|
|
261
|
+
Define se a ferramenta requer credenciais para funcionar.
|
|
262
|
+
</p>
|
|
263
|
+
{form.typeError && (
|
|
264
|
+
<p className="text-sm text-destructive">Tipo é obrigatório</p>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div className="space-y-2">
|
|
269
|
+
<Label htmlFor="tool-description">Descrição</Label>
|
|
270
|
+
<Textarea
|
|
271
|
+
id="tool-description"
|
|
272
|
+
value={form.description}
|
|
273
|
+
onChange={(e) =>
|
|
274
|
+
setForm((prev) => ({ ...prev, description: e.target.value }))
|
|
275
|
+
}
|
|
276
|
+
placeholder="Descrição da ferramenta..."
|
|
277
|
+
rows={3}
|
|
278
|
+
disabled={isPending}
|
|
279
|
+
/>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div className="space-y-2">
|
|
283
|
+
<Label htmlFor="tool-function-defs">
|
|
284
|
+
Definições de Função (JSON)
|
|
285
|
+
</Label>
|
|
286
|
+
<Textarea
|
|
287
|
+
id="tool-function-defs"
|
|
288
|
+
value={form.functionDefinitions}
|
|
289
|
+
onChange={(e) => {
|
|
290
|
+
setForm((prev) => ({
|
|
291
|
+
...prev,
|
|
292
|
+
functionDefinitions: e.target.value,
|
|
293
|
+
jsonError: false,
|
|
294
|
+
}));
|
|
295
|
+
}}
|
|
296
|
+
placeholder={`[
|
|
297
|
+
{
|
|
298
|
+
"type": "function",
|
|
299
|
+
"function": {
|
|
300
|
+
"name": "nome_da_funcao",
|
|
301
|
+
"description": "O que a função faz",
|
|
302
|
+
"parameters": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"properties": { ... },
|
|
305
|
+
"required": [...]
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
]`}
|
|
310
|
+
rows={10}
|
|
311
|
+
className="font-mono text-sm"
|
|
312
|
+
disabled={isPending}
|
|
313
|
+
/>
|
|
314
|
+
<p className="text-xs text-muted-foreground">
|
|
315
|
+
Array de definições no formato OpenAI Function Calling.
|
|
316
|
+
</p>
|
|
317
|
+
{form.jsonError && (
|
|
318
|
+
<p className="text-sm text-destructive">JSON inválido</p>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<DialogFooter>
|
|
323
|
+
<Button
|
|
324
|
+
type="button"
|
|
325
|
+
variant="outline"
|
|
326
|
+
onClick={() => onOpenChange(false)}
|
|
327
|
+
disabled={isPending}
|
|
328
|
+
>
|
|
329
|
+
Cancelar
|
|
330
|
+
</Button>
|
|
331
|
+
<Button type="submit" disabled={isPending}>
|
|
332
|
+
{isPending ? (
|
|
333
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
334
|
+
) : null}
|
|
335
|
+
{isEditing ? "Salvar" : "Criar"}
|
|
336
|
+
</Button>
|
|
337
|
+
</DialogFooter>
|
|
338
|
+
</form>
|
|
339
|
+
</DialogContent>
|
|
340
|
+
</Dialog>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import type { ColumnDef } from "@tanstack/react-table";
|
|
3
|
+
import { useTools, useDeleteTool } from "../../hooks";
|
|
4
|
+
import type { Tool } from "../../types";
|
|
5
|
+
import type { GagentsHookConfig } from "../../hooks/types";
|
|
6
|
+
import { DataTable } from "@greatapps/greatauth-ui";
|
|
7
|
+
import {
|
|
8
|
+
Input,
|
|
9
|
+
Badge,
|
|
10
|
+
Tooltip,
|
|
11
|
+
TooltipTrigger,
|
|
12
|
+
TooltipContent,
|
|
13
|
+
AlertDialog,
|
|
14
|
+
AlertDialogAction,
|
|
15
|
+
AlertDialogCancel,
|
|
16
|
+
AlertDialogContent,
|
|
17
|
+
AlertDialogDescription,
|
|
18
|
+
AlertDialogFooter,
|
|
19
|
+
AlertDialogHeader,
|
|
20
|
+
AlertDialogTitle,
|
|
21
|
+
Button,
|
|
22
|
+
} from "@greatapps/greatauth-ui/ui";
|
|
23
|
+
import { Pencil, Trash2, Search } from "lucide-react";
|
|
24
|
+
import { format } from "date-fns";
|
|
25
|
+
import { ptBR } from "date-fns/locale";
|
|
26
|
+
import { toast } from "sonner";
|
|
27
|
+
|
|
28
|
+
interface ToolsTableProps {
|
|
29
|
+
onEdit: (tool: Tool) => void;
|
|
30
|
+
config: GagentsHookConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function useColumns(
|
|
34
|
+
onEdit: (tool: Tool) => void,
|
|
35
|
+
onDelete: (id: number) => void,
|
|
36
|
+
): ColumnDef<Tool>[] {
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
accessorKey: "name",
|
|
40
|
+
header: "Nome",
|
|
41
|
+
cell: ({ row }) => (
|
|
42
|
+
<span className="font-medium">{row.original.name}</span>
|
|
43
|
+
),
|
|
44
|
+
sortingFn: (rowA, rowB) =>
|
|
45
|
+
rowA.original.name
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.localeCompare(rowB.original.name.toLowerCase()),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
accessorKey: "slug",
|
|
51
|
+
header: "Slug",
|
|
52
|
+
cell: ({ row }) => (
|
|
53
|
+
<span className="text-muted-foreground text-sm font-mono">
|
|
54
|
+
{row.original.slug || "\u2014"}
|
|
55
|
+
</span>
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
accessorKey: "type",
|
|
60
|
+
header: "Tipo",
|
|
61
|
+
cell: ({ row }) => (
|
|
62
|
+
<Badge variant="secondary">{row.original.type}</Badge>
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
accessorKey: "description",
|
|
67
|
+
header: "Descrição",
|
|
68
|
+
cell: ({ row }) => {
|
|
69
|
+
const desc = row.original.description;
|
|
70
|
+
if (!desc) return <span className="text-muted-foreground text-sm">{"\u2014"}</span>;
|
|
71
|
+
return (
|
|
72
|
+
<span className="text-muted-foreground text-sm">
|
|
73
|
+
{desc.length > 50 ? `${desc.slice(0, 50)}...` : desc}
|
|
74
|
+
</span>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
accessorKey: "datetime_add",
|
|
80
|
+
header: "Criado em",
|
|
81
|
+
cell: ({ row }) => (
|
|
82
|
+
<span className="text-muted-foreground text-sm">
|
|
83
|
+
{format(new Date(row.original.datetime_add), "dd/MM/yyyy", {
|
|
84
|
+
locale: ptBR,
|
|
85
|
+
})}
|
|
86
|
+
</span>
|
|
87
|
+
),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "actions",
|
|
91
|
+
size: 80,
|
|
92
|
+
enableSorting: false,
|
|
93
|
+
cell: ({ row }) => (
|
|
94
|
+
<div className="flex items-center gap-1">
|
|
95
|
+
<Tooltip>
|
|
96
|
+
<TooltipTrigger asChild>
|
|
97
|
+
<Button
|
|
98
|
+
variant="ghost"
|
|
99
|
+
size="icon"
|
|
100
|
+
className="h-8 w-8"
|
|
101
|
+
onClick={() => onEdit(row.original)}
|
|
102
|
+
>
|
|
103
|
+
<Pencil className="h-4 w-4" />
|
|
104
|
+
</Button>
|
|
105
|
+
</TooltipTrigger>
|
|
106
|
+
<TooltipContent>Editar</TooltipContent>
|
|
107
|
+
</Tooltip>
|
|
108
|
+
<Tooltip>
|
|
109
|
+
<TooltipTrigger asChild>
|
|
110
|
+
<Button
|
|
111
|
+
variant="ghost"
|
|
112
|
+
size="icon"
|
|
113
|
+
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
114
|
+
onClick={() => onDelete(row.original.id)}
|
|
115
|
+
>
|
|
116
|
+
<Trash2 className="h-4 w-4" />
|
|
117
|
+
</Button>
|
|
118
|
+
</TooltipTrigger>
|
|
119
|
+
<TooltipContent>Excluir</TooltipContent>
|
|
120
|
+
</Tooltip>
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function ToolsTable({ onEdit, config }: ToolsTableProps) {
|
|
128
|
+
const [search, setSearch] = useState("");
|
|
129
|
+
const [page, setPage] = useState(1);
|
|
130
|
+
|
|
131
|
+
const queryParams = useMemo(() => {
|
|
132
|
+
const params: Record<string, string> = {
|
|
133
|
+
limit: "15",
|
|
134
|
+
page: String(page),
|
|
135
|
+
};
|
|
136
|
+
if (search) {
|
|
137
|
+
params.search = search;
|
|
138
|
+
}
|
|
139
|
+
return params;
|
|
140
|
+
}, [search, page]);
|
|
141
|
+
|
|
142
|
+
const { data, isLoading } = useTools(config, queryParams);
|
|
143
|
+
const deleteTool = useDeleteTool(config);
|
|
144
|
+
const [deleteId, setDeleteId] = useState<number | null>(null);
|
|
145
|
+
|
|
146
|
+
const tools = data?.data || [];
|
|
147
|
+
const total = data?.total || 0;
|
|
148
|
+
|
|
149
|
+
const columns = useColumns(
|
|
150
|
+
(tool) => onEdit(tool),
|
|
151
|
+
(id) => setDeleteId(id),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
function handleDelete() {
|
|
155
|
+
if (!deleteId) return;
|
|
156
|
+
deleteTool.mutate(deleteId, {
|
|
157
|
+
onSuccess: () => {
|
|
158
|
+
toast.success("Ferramenta excluída");
|
|
159
|
+
setDeleteId(null);
|
|
160
|
+
},
|
|
161
|
+
onError: () => toast.error("Erro ao excluir ferramenta"),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function handleSearchChange(value: string) {
|
|
166
|
+
setSearch(value);
|
|
167
|
+
setPage(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<>
|
|
172
|
+
<div className="flex items-center gap-3">
|
|
173
|
+
<div className="relative flex-1 max-w-md">
|
|
174
|
+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
175
|
+
<Input
|
|
176
|
+
placeholder="Buscar ferramentas..."
|
|
177
|
+
value={search}
|
|
178
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
179
|
+
className="pl-9"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<DataTable
|
|
185
|
+
columns={columns}
|
|
186
|
+
data={tools}
|
|
187
|
+
isLoading={isLoading}
|
|
188
|
+
emptyMessage="Nenhuma ferramenta encontrada"
|
|
189
|
+
total={total}
|
|
190
|
+
page={page}
|
|
191
|
+
onPageChange={setPage}
|
|
192
|
+
pageSize={15}
|
|
193
|
+
/>
|
|
194
|
+
|
|
195
|
+
{/* Delete confirmation */}
|
|
196
|
+
<AlertDialog
|
|
197
|
+
open={!!deleteId}
|
|
198
|
+
onOpenChange={(open) => !open && setDeleteId(null)}
|
|
199
|
+
>
|
|
200
|
+
<AlertDialogContent>
|
|
201
|
+
<AlertDialogHeader>
|
|
202
|
+
<AlertDialogTitle>Excluir ferramenta?</AlertDialogTitle>
|
|
203
|
+
<AlertDialogDescription>
|
|
204
|
+
Esta ação não pode ser desfeita. A ferramenta será removida
|
|
205
|
+
permanentemente.
|
|
206
|
+
</AlertDialogDescription>
|
|
207
|
+
</AlertDialogHeader>
|
|
208
|
+
<AlertDialogFooter>
|
|
209
|
+
<AlertDialogCancel variant="outline" size="default">
|
|
210
|
+
Cancelar
|
|
211
|
+
</AlertDialogCancel>
|
|
212
|
+
<AlertDialogAction
|
|
213
|
+
variant="default"
|
|
214
|
+
size="default"
|
|
215
|
+
onClick={handleDelete}
|
|
216
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
217
|
+
>
|
|
218
|
+
Excluir
|
|
219
|
+
</AlertDialogAction>
|
|
220
|
+
</AlertDialogFooter>
|
|
221
|
+
</AlertDialogContent>
|
|
222
|
+
</AlertDialog>
|
|
223
|
+
</>
|
|
224
|
+
);
|
|
225
|
+
}
|