All files / components/Courses/TabComponent EdgeConceptTabComponent.jsx

100% Statements 37/37
90.9% Branches 10/11
100% Functions 14/14
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167            2x     73x 73x 73x   73x 73x             73x 73x             73x 88x     73x           73x     6x           73x       2x 2x 2x       4x   4x 4x           73x           73x       2x           73x 6x 6x 6x       73x       73x                                     6x       88x                       7x       88x                                                 2x 2x              
import React, { useState } from "react";
import { Alert, Button, Form } from "react-bootstrap";
import { toast } from "react-toastify";
import EdgeTable from "main/components/Concept/EdgeTable";
import { useBackend, useBackendMutation } from "main/utils/useBackend";
 
const suppressFetchToasts = true;
 
export default function EdgeConceptTabComponent({ courseId, testIdPrefix }) {
  const [sourceConceptId, setSourceConceptId] = useState("");
  const [targetConceptId, setTargetConceptId] = useState("");
  const [createEdgeError, setCreateEdgeError] = useState(null);
 
  const conceptsPath = `/api/concepts/course?courseId=${courseId}`;
  const { data: concepts } = useBackend(
    [conceptsPath],
    { method: "GET", url: conceptsPath },
    [],
    suppressFetchToasts,
  );
 
  const edgesPath = `/api/concepts/edges?courseId=${courseId}`;
  const { data: edges } = useBackend(
    [edgesPath],
    { method: "GET", url: edgesPath },
    [],
    suppressFetchToasts,
  );
 
  const labelById = new Map(
    concepts.map((concept) => [concept.id, concept.label]),
  );
 
  const edgesWithLabels = edges.map((edge) => ({
    ...edge,
    sourceLabel: labelById.get(edge.sourceId) ?? `id ${edge.sourceId}`,
    targetLabel: labelById.get(edge.targetId) ?? `id ${edge.targetId}`,
  }));
 
  const createEdgeObjectToAxiosParams = ({
    sourceConceptId,
    targetConceptId,
  }) => ({
    url: "/api/concepts/edges/post",
    method: "POST",
    params: { sourceConceptId, targetConceptId },
  });
 
  const createEdgeMutation = useBackendMutation(
    createEdgeObjectToAxiosParams,
    {
      onSuccess: () => {
        toast("Edge created");
        setSourceConceptId("");
        setTargetConceptId("");
      },
      onError: (error) => {
        const message =
          error.response?.data?.message ??
          "Error creating edge; please try again";
        setCreateEdgeError(message);
        toast(message);
      },
    },
    [edgesPath],
  );
 
  const deleteEdgeObjectToAxiosParams = (cell) => ({
    url: "/api/concepts/edges/delete",
    method: "DELETE",
    params: { id: cell.row.original.id },
  });
 
  const deleteEdgeMutation = useBackendMutation(
    deleteEdgeObjectToAxiosParams,
    {
      onSuccess: () => {
        toast("Edge deleted");
      },
    },
    [edgesPath],
  );
 
  const handleCreateEdge = (event) => {
    event.preventDefault();
    setCreateEdgeError(null);
    createEdgeMutation.mutate({ sourceConceptId, targetConceptId });
  };
 
  const createDisabled =
    sourceConceptId === "" ||
    targetConceptId === "" ||
    sourceConceptId === targetConceptId;
 
  return (
    <div
      className="tabComponent"
      data-testid={`${testIdPrefix}-edgeConceptTab`}
    >
      <h2>Edges</h2>
      <p>
        Edges represent prerequisite relationships between top-level concepts.
        Select a &quot;from&quot; concept (the prerequisite) and a
        &quot;to&quot; concept (the concept that depends on it), then click
        Create Edge.
      </p>
      <Form onSubmit={handleCreateEdge} className="mb-3">
        <Form.Group className="mb-2">
          <Form.Label htmlFor="sourceConceptId">From (source)</Form.Label>
          <Form.Select
            id="sourceConceptId"
            data-testid={`${testIdPrefix}-source-select`}
            value={sourceConceptId}
            onChange={(event) => setSourceConceptId(event.target.value)}
          >
            <option value="">Select a concept</option>
            {concepts.map((concept) => (
              <option key={concept.id} value={concept.id}>
                {concept.label}
              </option>
            ))}
          </Form.Select>
        </Form.Group>
        <Form.Group className="mb-2">
          <Form.Label htmlFor="targetConceptId">To (target)</Form.Label>
          <Form.Select
            id="targetConceptId"
            data-testid={`${testIdPrefix}-target-select`}
            value={targetConceptId}
            onChange={(event) => setTargetConceptId(event.target.value)}
          >
            <option value="">Select a concept</option>
            {concepts.map((concept) => (
              <option key={concept.id} value={concept.id}>
                {concept.label}
              </option>
            ))}
          </Form.Select>
        </Form.Group>
        {createEdgeError && (
          <Alert
            variant="danger"
            data-testid={`${testIdPrefix}-create-edge-error`}
          >
            {createEdgeError}
          </Alert>
        )}
        <Button
          type="submit"
          disabled={createDisabled}
          data-testid={`${testIdPrefix}-create-edge-button`}
        >
          Create Edge
        </Button>
      </Form>
      <EdgeTable
        edges={edgesWithLabels}
        deleteCallback={(cell) => {
          setCreateEdgeError(null);
          deleteEdgeMutation.mutate(cell);
        }}
        testId={`${testIdPrefix}-EdgeTable`}
      />
    </div>
  );
}