wp-graphql/wp-graphql

View on GitHub
packages/graphiql-query-composer/components/QueryBuilder.js

Summary

Maintainability
F
3 days
Test Coverage
import {
  defaultGetDefaultFieldNames,
  defaultGetDefaultScalarArgValue,
  defaultColors,
  defaultStyles,
  defaultCheckboxChecked,
  defaultCheckboxUnchecked,
  defaultArrowClosed,
  defaultArrowOpen,
  memoizeParseQuery,
  DEFAULT_DOCUMENT,
  capitalize,
} from "../utils/utils";
import RootView from "./RootView";
import AddOperations from "./AddOperations";

const { useAppContext } = wpGraphiQL;
const { GraphQLObjectType, print } = wpGraphiQL.GraphQL;

const { useState, useEffect, useRef } = wp.element;

const QueryBuilder = (props) => {
  const [newOperationType, setNewOperationType] = useState("query");
  const [operation, setOperation] = useState(null);
  const [operationToScrollTo, setOperationToScrollTo] = useState(null);

  let container = useRef(null);

  const _resetScroll = () => {
    if (container) {
      container.scrollLeft = 0;
    }
  };

  useEffect(() => {
    // _resetScroll()
  });

  const _onEdit = (query) => props.onEdit(query);

  const _setAddOperationType = (value) => {
    setNewOperationType(value);
  };

  const _handleRootViewMount = (rootViewElId) => {
    if (!!operationToScrollTo && operationToScrollTo === rootViewElId) {
      // let selector = `.graphiql-explorer-root #${rootViewElId}`;
      //
      // let el = document.querySelector(selector);
      // el && el.scrollIntoView();
    }
  };

  const { schema, query } = useAppContext();
  const { makeDefaultArg } = props;

  if (!schema) {
    return (
      <div style={{ fontFamily: "sans-serif" }} className="error-container">
        No Schema Available
      </div>
    );
  }

  const styleConfig = {
    colors: props.colors || defaultColors,
    checkboxChecked: props.checkboxChecked || defaultCheckboxChecked,
    checkboxUnchecked: props.checkboxUnchecked || defaultCheckboxUnchecked,
    arrowClosed: props.arrowClosed || defaultArrowClosed,
    arrowOpen: props.arrowOpen || defaultArrowOpen,
    styles: props.styles
      ? {
          ...defaultStyles,
          ...props.styles,
        }
      : defaultStyles,
  };

  const queryType = schema.getQueryType();
  const mutationType = schema.getMutationType();
  const subscriptionType = schema.getSubscriptionType();

  if (!queryType && !mutationType && !subscriptionType) {
    return <div>Missing query type</div>;
  }
  const queryFields = queryType && queryType.getFields();
  const mutationFields = mutationType && mutationType.getFields();
  const subscriptionFields = subscriptionType && subscriptionType.getFields();

  const parsedQuery = memoizeParseQuery(query);

  const getDefaultFieldNames =
    props.getDefaultFieldNames || defaultGetDefaultFieldNames;

  const getDefaultScalarArgValue =
    props.getDefaultScalarArgValue || defaultGetDefaultScalarArgValue;

  const definitions = parsedQuery.definitions;

  const _relevantOperations = definitions
    .map((definition) => {
      if (definition.kind === "FragmentDefinition") {
        return definition;
      } else if (definition.kind === "OperationDefinition") {
        return definition;
      } else {
        return null;
      }
    })
    .filter(Boolean);

  const relevantOperations =
    // If we don't have any relevant definitions from the parsed document,
    // then at least show an expanded Query selection
    _relevantOperations.length === 0
      ? DEFAULT_DOCUMENT.definitions
      : _relevantOperations;

  const renameOperation = (targetOperation, name) => {
    const newName =
      name == null || name === ""
        ? null
        : { kind: "Name", value: name, loc: undefined };
    const newOperation = { ...targetOperation, name: newName };

    const existingDefs = parsedQuery.definitions;

    const newDefinitions = existingDefs.map((existingOperation) => {
      if (targetOperation === existingOperation) {
        return newOperation;
      } else {
        return existingOperation;
      }
    });

    return {
      ...parsedQuery,
      definitions: newDefinitions,
    };
  };

  const cloneOperation = (targetOperation) => {
    let kind;
    if (targetOperation.kind === "FragmentDefinition") {
      kind = "fragment";
    } else {
      kind = targetOperation.operation;
    }

    const newOperationName =
      ((targetOperation.name && targetOperation.name.value) || "") + "Copy";

    const newName = {
      kind: "Name",
      value: newOperationName,
      loc: undefined,
    };

    const newOperation = { ...targetOperation, name: newName };

    const existingDefs = parsedQuery.definitions;

    const newDefinitions = [...existingDefs, newOperation];

    // setOperationToScrollTo(`${kind}-${newOperationName}`);

    return {
      ...parsedQuery,
      definitions: newDefinitions,
    };
  };

  const destroyOperation = (targetOperation) => {
    const existingDefs = parsedQuery.definitions;

    const newDefinitions = existingDefs.filter((existingOperation) => {
      return targetOperation !== existingOperation;
    });

    return {
      ...parsedQuery,
      definitions: newDefinitions,
    };
  };

  const addOperation = (kind) => {
    const existingDefs = parsedQuery.definitions;

    const viewingDefaultOperation =
      parsedQuery.definitions.length === 1 &&
      parsedQuery.definitions[0] === DEFAULT_DOCUMENT.definitions[0];

    const MySiblingDefs = viewingDefaultOperation
      ? []
      : existingDefs.filter((def) => {
          if (def.kind === "OperationDefinition") {
            return def.operation === kind;
          } else {
            // Don't support adding fragments from explorer
            return false;
          }
        });

    const newOperationName = `My${capitalize(kind)}${
      MySiblingDefs.length === 0 ? "" : MySiblingDefs.length + 1
    }`;

    // Add this as the default field as it guarantees a valid selectionSet
    const firstFieldName = "__typename # Placeholder value";

    const selectionSet = {
      kind: "SelectionSet",
      selections: [
        {
          kind: "Field",
          name: {
            kind: "Name",
            value: firstFieldName,
            loc: null,
          },
          arguments: [],
          directives: [],
          selectionSet: null,
          loc: null,
        },
      ],
      loc: null,
    };

    const newDefinition = {
      kind: "OperationDefinition",
      operation: kind,
      name: { kind: "Name", value: newOperationName },
      variableDefinitions: [],
      directives: [],
      selectionSet: selectionSet,
      loc: null,
    };

    const newDefinitions =
      // If we only have our default operation in the document right now, then
      // just replace it with our new definition
      viewingDefaultOperation
        ? [newDefinition]
        : [...parsedQuery.definitions, newDefinition];

    const newOperationDef = {
      ...parsedQuery,
      definitions: newDefinitions,
    };

    // setOperationToScrollTo(`${kind}-${newOperationName}`);

    props.onEdit(print(newOperationDef));
  };

  let actionsOptions = [];

  if (queryFields) {
    actionsOptions.push({
      type: `query`,
      label: `Queries`,
      fields: () => {
        return queryFields;
      },
    });
  }

  if (subscriptionFields) {
    actionsOptions.push({
      type: `subscription`,
      label: `Subscriptions`,
      fields: () => {
        return subscriptionFields;
      },
    });
  }

  if (mutationFields) {
    actionsOptions.push({
      type: `mutation`,
      label: `Mutations`,
      fields: () => {
        return mutationFields;
      },
    });
  }

  const actionsEl = (
    <AddOperations
      query={query}
      actionOptions={actionsOptions}
      addOperation={addOperation}
    />
  );

  const availableFragments = relevantOperations.reduce((acc, operation) => {
    if (operation.kind === "FragmentDefinition") {
      const fragmentTypeName = operation.typeCondition.name.value;
      const existingFragmentsForType = acc[fragmentTypeName] || [];
      const newFragmentsForType = [
        ...existingFragmentsForType,
        operation,
      ].sort((a, b) => a.name.value.localeCompare(b.name.value));
      return {
        ...acc,
        [fragmentTypeName]: newFragmentsForType,
      };
    }

    return acc;
  }, {});

  return (
    <div
      ref={(node) => {
        container = node;
      }}
      style={{
        fontSize: 12,
        textOverflow: "ellipsis",
        whiteSpace: "nowrap",
        margin: 0,
        padding: 0,
        fontFamily:
          'Consolas, Inconsolata, "Droid Sans Mono", Monaco, monospace',
        display: "flex",
        flexDirection: "column",
        height: "100%",
      }}
      className="graphiql-explorer-root antd-app"
    >
      <div
        style={{
          flexGrow: 1,
          overflowY: "scroll",
          width: `100%`,
          padding: "8px",
        }}
      >
        {relevantOperations.map((operation, index) => {
          const operationName =
            operation && operation.name && operation.name.value;

          const operationType =
            operation.kind === "FragmentDefinition"
              ? "fragment"
              : (operation && operation.operation) || "query";

          const onOperationRename = (newName) => {
            const newOperationDef = renameOperation(operation, newName);
            props.onEdit(print(newOperationDef));
          };

          const onOperationClone = () => {
            const newOperationDef = cloneOperation(operation);
            props.onEdit(print(newOperationDef));
          };

          const onOperationDestroy = () => {
            const newOperationDef = destroyOperation(operation);
            props.onEdit(print(newOperationDef));
          };

          const fragmentType =
            operation.kind === "FragmentDefinition" &&
            operation.typeCondition.kind === "NamedType" &&
            schema.getType(operation.typeCondition.name.value);

          const fragmentFields =
            fragmentType instanceof GraphQLObjectType
              ? fragmentType.getFields()
              : null;

          const fields =
            operationType === "query"
              ? queryFields
              : operationType === "mutation"
              ? mutationFields
              : operationType === "subscription"
              ? subscriptionFields
              : operation.kind === "FragmentDefinition"
              ? fragmentFields
              : null;

          const fragmentTypeName =
            operation.kind === "FragmentDefinition"
              ? operation.typeCondition.name.value
              : null;

          const onCommit = (parsedDocument) => {
            const textualNewDocument = print(parsedDocument);

            props.onEdit(textualNewDocument);
          };

          return (
            <RootView
              key={index}
              index={index}
              isLast={index === relevantOperations.length - 1}
              fields={fields}
              operationType={operationType}
              name={operationName}
              definition={operation}
              onOperationRename={onOperationRename}
              onOperationDestroy={onOperationDestroy}
              onOperationClone={onOperationClone}
              onTypeName={fragmentTypeName}
              onMount={_handleRootViewMount}
              onCommit={onCommit}
              onEdit={(newDefinition, options) => {
                let commit;
                if (
                  typeof options === "object" &&
                  typeof options.commit !== "undefined"
                ) {
                  commit = options.commit;
                } else {
                  commit = true;
                }

                if (!!newDefinition) {
                  const newQuery = {
                    ...parsedQuery,
                    definitions: parsedQuery.definitions.map(
                      (existingDefinition) =>
                        existingDefinition === operation
                          ? newDefinition
                          : existingDefinition
                    ),
                  };

                  if (commit) {
                    onCommit(newQuery);
                    return newQuery;
                  } else {
                    return newQuery;
                  }
                } else {
                  return parsedQuery;
                }
              }}
              schema={schema}
              getDefaultFieldNames={getDefaultFieldNames}
              getDefaultScalarArgValue={getDefaultScalarArgValue}
              makeDefaultArg={makeDefaultArg}
              onRunOperation={() => {
                if (!!props.onRunOperation) {
                  props.onRunOperation(operationName);
                }
              }}
              styleConfig={styleConfig}
              availableFragments={availableFragments}
            />
          );
        })}
      </div>
      {actionsEl}
    </div>
  );
};

export default QueryBuilder;