()
| 17 | }`; |
| 18 | |
| 19 | export function QueryEditor() { |
| 20 | const [query, setQuery] = useState(EXAMPLE_QUERY); |
| 21 | const [result, setResult] = useState<QueryResult | null>(null); |
| 22 | const { toast } = useToast(); |
| 23 | |
| 24 | const executeMutation = useMutation({ |
| 25 | mutationFn: async (graphqlQuery: string) => { |
| 26 | const res = await apiRequest("POST", "/api/graphql", { query: graphqlQuery }); |
| 27 | return await res.json() as QueryResult; |
| 28 | }, |
| 29 | onSuccess: (data) => { |
| 30 | setResult(data); |
| 31 | }, |
| 32 | onError: (error: Error) => { |
| 33 | toast({ |
| 34 | title: "Query Thất Bại", |
| 35 | description: error.message, |
| 36 | variant: "destructive", |
| 37 | }); |
| 38 | }, |
| 39 | }); |
| 40 | |
| 41 | const handleExecute = () => { |
| 42 | executeMutation.mutate(query); |
| 43 | }; |
| 44 | |
| 45 | const handleCopy = async () => { |
| 46 | if (result) { |
| 47 | await navigator.clipboard.writeText(JSON.stringify(result, null, 2)); |
| 48 | toast({ |
| 49 | title: "Đã Sao Chép!", |
| 50 | description: "Response đã được sao chép vào clipboard", |
| 51 | }); |
| 52 | } |
| 53 | }; |
| 54 | |
| 55 | const handleClear = () => { |
| 56 | setQuery(""); |
| 57 | setResult(null); |
| 58 | }; |
| 59 | |
| 60 | return ( |
| 61 | <div className="container mx-auto px-4 py-8"> |
| 62 | <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| 63 | <div className="space-y-4"> |
| 64 | <div className="flex items-center justify-between"> |
| 65 | <h2 className="text-xl font-semibold flex items-center gap-2"> |
| 66 | <span className="w-2 h-2 rounded-full bg-primary animate-pulse" /> |
| 67 | Trình Soạn Query |
| 68 | </h2> |
| 69 | <div className="flex gap-2"> |
| 70 | <Button |
| 71 | size="sm" |
| 72 | variant="outline" |
| 73 | onClick={handleClear} |
| 74 | data-testid="button-clear" |
| 75 | > |
| 76 | <Trash2 className="w-4 h-4" /> |
nothing calls this directly
no test coverage detected