Skip to content

Commit

Permalink
Inline filter save (#615)
Browse files Browse the repository at this point in the history
* Add inline filter save

* Add disable

* Fix code style issues with ESLint

* Lint fixes

---------

Co-authored-by: Lint Action <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
edlouth and github-actions[bot] authored Aug 23, 2023
1 parent e810f00 commit 553342a
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -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<CreateFilterFormProps> = ({
loading,
onSubmit,
}) => {
const [values, setValues] = useState<Values>({
name: "",
})

const handleSubmit = () => onSubmit(values)

return (
<Form onSubmit={handleSubmit}>
<TextField
label="Name"
value={values.name}
onChange={e => setValues({ ...values, name: e.target.value })}
fullWidth
required
margin="normal"
/>
<LoadingButton
loading={loading}
type="submit"
variant="contained"
fullWidth
sx={{ mt: 2, height: 56 }}
>
Save
</LoadingButton>
</Form>
)
}

export default CreateFilterForm
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -87,11 +87,7 @@ const GraphFilterInline: React.FC<GraphFilterInlineProps> = ({

return (
<Box sx={{ p: 1 }}>
<Stack direction="row" spacing={1} sx={{ mb: 1 }}>
<Button variant="outlined" fullWidth startIcon={<Save />}>
Save
</Button>
</Stack>
<SaveButton inlineFilters={inlineFilters} workspaceId={workspace.id} />
{inlineFilters.map((filter, index) => (
<FilterRow
key={index}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import userEvent from "@testing-library/user-event"
import { act, render, screen, waitFor } from "testing"
import SaveButton from "./SaveButton"

const defaultProps = {
workspaceId: "1",
inlineFilters: [
{
type: "table",
field: null,
operator: null,
value: null,
},
],
}

test("renders", async () => {
render(<SaveButton {...defaultProps} />)

await waitFor(() => {
expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument()
})
})

test("open and close", async () => {
const user = userEvent.setup()

render(<SaveButton {...defaultProps} />)

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")))
})
Original file line number Diff line number Diff line change
@@ -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<SaveButtonProps> = ({
workspaceId,
inlineFilters,
}) => {
const [open, setOpen] = useState(false)

const handleOpen = () => setOpen(true)
const handleClose = () => setOpen(false)

return (
<>
<Button
variant="outlined"
fullWidth
startIcon={<Save />}
sx={{ mb: 1 }}
onClick={handleOpen}
disabled={inlineFilters.length === 0}
>
Save
</Button>
<SaveDialog
open={open}
onClose={handleClose}
workspaceId={workspaceId}
inlineFilters={inlineFilters}
/>
</>
)
}

export default SaveButton
Original file line number Diff line number Diff line change
@@ -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(<SaveDialog {...defaultProps} />)

await waitFor(() => {
expect(screen.getByRole("button", { name: /save/i })).toBeInTheDocument()
})
})

test("submit", async () => {
const user = userEvent.setup()

render(<SaveDialog {...defaultProps} />)

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(<SaveDialog {...defaultProps} />, { 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()
})
})
Original file line number Diff line number Diff line change
@@ -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<SaveDialogProps> = ({
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<NewFilter>({
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 (
<Dialog open={open} onClose={onClose} fullWidth maxWidth="sm">
<DialogTitle onClose={onClose}>Save Filter</DialogTitle>
<DialogContent>
{error && <GraphError error={error} />}
<CreateFilterForm onSubmit={handleSave} loading={loading} />
</DialogContent>
</Dialog>
)
}

export default SaveDialog

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 553342a

Please sign in to comment.