import React, { useEffect, useCallback, useMemo, useRef } from 'react'; import find from 'lodash/find'; import get from 'lodash/get'; import classnames from 'classnames'; import { IconWand, IconDots, IconBook, IconDownload, IconRefresh, IconFile, IconChevronDown, IconChevronRight } from '@tabler/icons'; import IconSidebarToggle from 'components/Icons/IconSidebarToggle'; import ActionIcon from 'ui/ActionIcon'; import { useSelector, useDispatch } from 'react-redux'; import { updateRequestPaneTab, updateQueryBuilderOpen, updateQueryBuilderWidth, updateVariablesPaneOpen, updateVariablesPaneHeight } from 'providers/ReduxStore/slices/tabs'; import QueryEditor from 'components/RequestPane/QueryEditor'; import QueryBuilder from 'components/RequestPane/QueryBuilder'; import MenuDropdown from 'ui/MenuDropdown'; import Auth from 'components/RequestPane/Auth'; import GraphQLVariables from 'components/RequestPane/GraphQLVariables'; import RequestHeaders from 'components/RequestPane/RequestHeaders'; import Vars from 'components/RequestPane/Vars'; import Assertions from 'components/RequestPane/Assertions'; import Script from 'components/RequestPane/Script'; import Tests from 'components/RequestPane/Tests'; import { useTheme } from 'providers/Theme'; import StyledWrapper from './StyledWrapper'; import { updateRequestGraphqlQuery, updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections'; import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import Documentation from 'components/Documentation/index'; import useGraphqlSchema from '../GraphQLSchemaActions/useGraphqlSchema'; import { findEnvironmentInCollection } from 'utils/collections'; import { hasEffectiveAuth } from 'utils/auth'; import HeightBoundContainer from 'ui/HeightBoundContainer'; import Settings from 'components/RequestPane/Settings'; import ResponsiveTabs from 'ui/ResponsiveTabs'; import AuthMode from '../Auth/AuthMode/index'; import StatusDot from 'components/StatusDot'; const TAB_CONFIG = [ { key: 'query', label: 'Query' }, { key: 'headers', label: 'Headers' }, { key: 'auth', label: 'Auth' }, { key: 'vars', label: 'Vars' }, { key: 'script', label: 'Script' }, { key: 'assert', label: 'Assert' }, { key: 'tests', label: 'Tests' }, { key: 'docs', label: 'Docs' }, { key: 'settings', label: 'Settings' } ]; const GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => { const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); const activeTabUid = useSelector((state) => state.tabs.activeTabUid); const preferences = useSelector((state) => state.app.preferences); const focusedTab = find(tabs, (t) => t.uid === activeTabUid); const requestPaneTab = focusedTab?.requestPaneTab; const showQueryBuilder = focusedTab?.queryBuilderOpen || false; const queryBuilderWidth = focusedTab?.queryBuilderWidth || 320; const variablesOpen = focusedTab?.variablesPaneOpen || false; const variablesHeight = focusedTab?.variablesPaneHeight || 150; const queryBuilderDraggingRef = useRef(false); const variablesDraggingRef = useRef(false); const queryBuilderContainerRef = useRef(null); const queryEditorRef = useRef(null); const query = item.draft ? get(item, 'draft.request.body.graphql.query', '') : get(item, 'request.body.graphql.query', ''); const variables = item.draft ? get(item, 'draft.request.body.graphql.variables', '') : get(item, 'request.body.graphql.variables', ''); const { displayedTheme } = useTheme(); const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', ''); const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', ''); const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', ''); const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid); const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid }; const { schema, schemaSource, loadSchema, isLoading: isSchemaLoading, error: schemaError } = useGraphqlSchema(url, environment, request, collection); const schemaActionsRef = useRef(null); useEffect(() => { onSchemaLoad(schema); }, [schema, onSchemaLoad]); const toggleQueryBuilder = useCallback(() => { dispatch(updateQueryBuilderOpen({ uid: item.uid, queryBuilderOpen: !showQueryBuilder })); }, [dispatch, item.uid, showQueryBuilder]); const variablesOpenRef = useRef(variablesOpen); variablesOpenRef.current = variablesOpen; const handleMouseMove = useCallback((e) => { if (queryBuilderDraggingRef.current && queryBuilderContainerRef.current) { e.preventDefault(); const containerRect = queryBuilderContainerRef.current.getBoundingClientRect(); const newWidth = e.clientX - containerRect.left; const maxWidth = Math.min(600, containerRect.width * 0.5); dispatch(updateQueryBuilderWidth({ uid: item.uid, queryBuilderWidth: Math.max(200, Math.min(newWidth, maxWidth)) })); } if (variablesDraggingRef.current && queryBuilderContainerRef.current) { e.preventDefault(); const containerRect = queryBuilderContainerRef.current.getBoundingClientRect(); // Subtract the header height (~30px) from the drag calculation const newHeight = containerRect.bottom - e.clientY - 30; if (newHeight < 40) { dispatch(updateVariablesPaneOpen({ uid: item.uid, variablesPaneOpen: false })); } else { if (!variablesOpenRef.current) dispatch(updateVariablesPaneOpen({ uid: item.uid, variablesPaneOpen: true })); dispatch(updateVariablesPaneHeight({ uid: item.uid, variablesPaneHeight: Math.max(80, Math.min(newHeight, containerRect.height * 0.6)) })); } } }, [dispatch, item.uid]); const handleMouseUp = useCallback(() => { queryBuilderDraggingRef.current = false; variablesDraggingRef.current = false; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }, [handleMouseMove]); const startDrag = useCallback((ref) => { ref.current = true; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); }, [handleMouseMove, handleMouseUp]); useEffect(() => { return () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; }, [handleMouseMove, handleMouseUp]); const onQueryChange = useCallback( (value) => { dispatch( updateRequestGraphqlQuery({ query: value, itemUid: item.uid, collectionUid: collection.uid }) ); }, [dispatch, item.uid, collection.uid] ); const onVariablesChange = useCallback( (value) => { dispatch( updateRequestGraphqlVariables({ variables: value, itemUid: item.uid, collectionUid: collection.uid }) ); }, [dispatch, item.uid, collection.uid] ); const onRun = useCallback( () => dispatch(sendRequest(item, collection.uid)), [dispatch, item, collection.uid] ); const onSave = useCallback( () => dispatch(saveRequest(item.uid, collection.uid)), [dispatch, item.uid, collection.uid] ); const selectTab = useCallback( (tabKey) => { dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey })); }, [dispatch, item.uid] ); const itemAuthMode = item.draft?.request?.auth?.mode ?? item.request?.auth?.mode ?? item.root?.request?.auth?.mode; const hasAuth = useMemo( () => hasEffectiveAuth(collection, item), [item, itemAuthMode, collection] ); const allTabs = useMemo( () => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: key === 'auth' && hasAuth ? : null })), [hasAuth] ); const handlePrettify = useCallback(() => { if (queryEditorRef.current?.beautifyRequestBody) { queryEditorRef.current.beautifyRequestBody(); } if (variables) { try { const pretty = JSON.stringify(JSON.parse(variables), null, 2); if (pretty !== variables) { onVariablesChange(pretty); } } catch { // Variables JSON is invalid, skip prettifying } } }, [variables, onVariablesChange]); const tabPanel = useMemo(() => { switch (requestPaneTab) { case 'query': return (
{ e.preventDefault(); startDrag(variablesDraggingRef); }} /> {variablesOpen && (
)}
); case 'headers': return ; case 'auth': return ; case 'vars': return ; case 'assert': return ; case 'script': return