diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/CreateFilterForm.tsx b/grai-frontend/src/components/graph/drawer/filters-inline/CreateFilterForm.tsx new file mode 100644 index 000000000..198d2c522 --- /dev/null +++ b/grai-frontend/src/components/graph/drawer/filters-inline/CreateFilterForm.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react" +import { LoadingButton } from "@mui/lab" +import { TextField } from "@mui/material" +import Form from "components/form/Form" + +export type Values = { + name: string +} + +type CreateFilterFormProps = { + loading?: boolean + onSubmit: (values: Values) => void +} + +const CreateFilterForm: React.FC = ({ + loading, + onSubmit, +}) => { + const [values, setValues] = useState({ + name: "", + }) + + const handleSubmit = () => onSubmit(values) + + return ( +
+ setValues({ ...values, name: e.target.value })} + fullWidth + required + margin="normal" + /> + + Save + + + ) +} + +export default CreateFilterForm diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/GraphFilterInline.tsx b/grai-frontend/src/components/graph/drawer/filters-inline/GraphFilterInline.tsx index 8a620e675..bdaf46776 100644 --- a/grai-frontend/src/components/graph/drawer/filters-inline/GraphFilterInline.tsx +++ b/grai-frontend/src/components/graph/drawer/filters-inline/GraphFilterInline.tsx @@ -1,13 +1,13 @@ import React from "react" import { gql, useQuery } from "@apollo/client" -import { Save } from "@mui/icons-material" -import { Box, Button, CircularProgress, Stack } from "@mui/material" +import { Box, CircularProgress } from "@mui/material" import NotFound from "pages/NotFound" import useWorkspace from "helpers/useWorkspace" import { Filter, getProperties } from "components/filters/filters" import GraphError from "components/utils/GraphError" import AddButton from "./AddButton" import FilterRow from "./FilterRow" +import SaveButton from "./SaveButton" import { GetWorkspaceFilterInline, GetWorkspaceFilterInlineVariables, @@ -87,11 +87,7 @@ const GraphFilterInline: React.FC = ({ return ( - - - + {inlineFilters.map((filter, index) => ( { + render() + + await waitFor(() => { + expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument() + }) +}) + +test("open and close", async () => { + const user = userEvent.setup() + + render() + + await waitFor(() => { + expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument() + }) + + await act( + async () => await user.click(screen.getByRole("button", { name: /save/i })), + ) + + await waitFor(() => { + expect(screen.getByTestId("CloseIcon")).toBeInTheDocument() + }) + + await act(async () => await user.click(screen.getByTestId("CloseIcon"))) +}) diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/SaveButton.tsx b/grai-frontend/src/components/graph/drawer/filters-inline/SaveButton.tsx new file mode 100644 index 000000000..4ac1a1b22 --- /dev/null +++ b/grai-frontend/src/components/graph/drawer/filters-inline/SaveButton.tsx @@ -0,0 +1,43 @@ +import React, { useState } from "react" +import { Save } from "@mui/icons-material" +import { Button } from "@mui/material" +import { Filter } from "components/filters/filters" +import SaveDialog from "./SaveDialog" + +type SaveButtonProps = { + workspaceId: string + inlineFilters: Filter[] +} + +const SaveButton: React.FC = ({ + workspaceId, + inlineFilters, +}) => { + const [open, setOpen] = useState(false) + + const handleOpen = () => setOpen(true) + const handleClose = () => setOpen(false) + + return ( + <> + + + + ) +} + +export default SaveButton diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/SaveDialog.test.tsx b/grai-frontend/src/components/graph/drawer/filters-inline/SaveDialog.test.tsx new file mode 100644 index 000000000..8092ff350 --- /dev/null +++ b/grai-frontend/src/components/graph/drawer/filters-inline/SaveDialog.test.tsx @@ -0,0 +1,71 @@ +import userEvent from "@testing-library/user-event" +import { GraphQLError } from "graphql" +import { act, render, screen, waitFor } from "testing" +import SaveDialog, { CREATE_FILTER } from "./SaveDialog" + +const defaultProps = { + open: true, + onClose: jest.fn(), + workspaceId: "1", + inlineFilters: [], +} + +test("renders", async () => { + render() + + await waitFor(() => { + expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument() + }) +}) + +test("submit", async () => { + const user = userEvent.setup() + + render() + + await waitFor(() => { + expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument() + }) + + await act(async () => user.type(screen.getByLabelText(/name/i), "test")) + + await act( + async () => await user.click(screen.getByRole("button", { name: /save/i })), + ) +}) + +test("error", async () => { + const user = userEvent.setup() + + const mocks = [ + { + request: { + query: CREATE_FILTER, + variables: { + workspaceId: "1", + name: "test", + metadata: [], + }, + }, + result: { + errors: [new GraphQLError("Error!")], + }, + }, + ] + + render(, { mocks }) + + await waitFor(() => { + expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument() + }) + + await act(async () => user.type(screen.getByLabelText(/name/i), "test")) + + await act( + async () => await user.click(screen.getByRole("button", { name: /save/i })), + ) + + await waitFor(() => { + expect(screen.getByText("Error!")).toBeInTheDocument() + }) +}) diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/SaveDialog.tsx b/grai-frontend/src/components/graph/drawer/filters-inline/SaveDialog.tsx new file mode 100644 index 000000000..655bb471b --- /dev/null +++ b/grai-frontend/src/components/graph/drawer/filters-inline/SaveDialog.tsx @@ -0,0 +1,103 @@ +import React from "react" +import { gql, useMutation } from "@apollo/client" +import { Dialog, DialogContent } from "@mui/material" +import { useSnackbar } from "notistack" +import DialogTitle from "components/dialogs/DialogTitle" +import { NewFilter } from "components/filters/__generated__/NewFilter" +import { Filter } from "components/filters/filters" +import GraphError from "components/utils/GraphError" +import { + CreateFilterInline, + CreateFilterInlineVariables, +} from "./__generated__/CreateFilterInline" +import CreateFilterForm, { Values } from "./CreateFilterForm" + +export const CREATE_FILTER = gql` + mutation CreateFilterInline( + $workspaceId: ID! + $name: String! + $metadata: JSON! + ) { + createFilter(workspaceId: $workspaceId, name: $name, metadata: $metadata) { + id + name + metadata + created_at + } + } +` + +type SaveDialogProps = { + workspaceId: string + inlineFilters: Filter[] + open: boolean + onClose: () => void +} + +const SaveDialog: React.FC = ({ + workspaceId, + inlineFilters, + open, + onClose, +}) => { + const { enqueueSnackbar } = useSnackbar() + + /* istanbul ignore next */ + const [createFilter, { loading, error }] = useMutation< + CreateFilterInline, + CreateFilterInlineVariables + >(CREATE_FILTER, { + update(cache, { data }) { + cache.modify({ + id: cache.identify({ + id: workspaceId, + __typename: "Workspace", + }), + fields: { + filters(existingFilters = { data: [] }) { + if (!data?.createFilter) return + + const newFilter = cache.writeFragment({ + data: data.createFilter, + fragment: gql` + fragment NewFilter on Filter { + id + name + metadata + created_at + } + `, + }) + return { + data: [...existingFilters.data, newFilter], + meta: { + total: (existingFilters.meta?.total ?? 0) + 1, + __typename: "FilterPagination", + }, + } + }, + }, + }) + }, + }) + + const handleSave = (values: Values) => + createFilter({ + variables: { workspaceId, metadata: inlineFilters, ...values }, + }) + .then(() => enqueueSnackbar("Filter created", { variant: "success" })) + .then(onClose) + .catch(() => {}) + + return ( + + Save Filter + + {error && } + + + + ) +} + +export default SaveDialog diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/__generated__/CreateFilterInline.ts b/grai-frontend/src/components/graph/drawer/filters-inline/__generated__/CreateFilterInline.ts new file mode 100644 index 000000000..462b3072e --- /dev/null +++ b/grai-frontend/src/components/graph/drawer/filters-inline/__generated__/CreateFilterInline.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: CreateFilterInline +// ==================================================== + +export interface CreateFilterInline_createFilter { + __typename: "Filter"; + id: any; + name: string | null; + metadata: any; + created_at: any; +} + +export interface CreateFilterInline { + createFilter: CreateFilterInline_createFilter; +} + +export interface CreateFilterInlineVariables { + workspaceId: string; + name: string; + metadata: any; +} diff --git a/grai-frontend/src/components/graph/drawer/filters-inline/__generated__/GetWorkspaceFilterInline.ts b/grai-frontend/src/components/graph/drawer/filters-inline/__generated__/GetWorkspaceFilterInline.ts new file mode 100644 index 000000000..078731729 --- /dev/null +++ b/grai-frontend/src/components/graph/drawer/filters-inline/__generated__/GetWorkspaceFilterInline.ts @@ -0,0 +1,47 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetWorkspaceFilterInline +// ==================================================== + +export interface GetWorkspaceFilterInline_workspace_namespaces { + __typename: "StrDataWrapper"; + data: string[]; +} + +export interface GetWorkspaceFilterInline_workspace_tags { + __typename: "StrDataWrapper"; + data: string[]; +} + +export interface GetWorkspaceFilterInline_workspace_sources_data { + __typename: "Source"; + id: any; + name: string; +} + +export interface GetWorkspaceFilterInline_workspace_sources { + __typename: "SourcePagination"; + data: GetWorkspaceFilterInline_workspace_sources_data[]; +} + +export interface GetWorkspaceFilterInline_workspace { + __typename: "Workspace"; + id: any; + name: string; + namespaces: GetWorkspaceFilterInline_workspace_namespaces; + tags: GetWorkspaceFilterInline_workspace_tags; + sources: GetWorkspaceFilterInline_workspace_sources; +} + +export interface GetWorkspaceFilterInline { + workspace: GetWorkspaceFilterInline_workspace; +} + +export interface GetWorkspaceFilterInlineVariables { + organisationName: string; + workspaceName: string; +}