Skip to content

Commit

Permalink
Fix EditDialog#onAfterSave not called on form submission (#1441)
Browse files Browse the repository at this point in the history
The `onAfterSave` callback was only called when submitting a form inside
an `EditDialog` by clicking the save button, but not when submitting the
form by hitting the enter key. We fix this by adding the callback to the
`EditDialogFormApi` and calling it after the form has been successfully
submitted.
  • Loading branch information
johnnyomair authored Nov 28, 2023
1 parent 167a804 commit e33cd65
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 71 deletions.
8 changes: 8 additions & 0 deletions .changeset/spotty-cougars-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@comet/admin": patch
---

Fix `EditDialog#onAfterSave` not called on form submission

The `onAfterSave` callback was only called when submitting a form inside an `EditDialog` by clicking the save button, but not when submitting the form by hitting the enter key.
We fix this by adding the callback to the `EditDialogFormApi` and calling it after the form has been successfully submitted.
10 changes: 9 additions & 1 deletion packages/admin/admin/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,13 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@comet/no-other-module-relative-import": "off"
}
},
"overrides": [
{
"files": ["*.test.ts", "*.test.tsx"],
"rules": {
"@calm/react-intl/missing-formatted-message": "off"
}
}
]
}
16 changes: 15 additions & 1 deletion packages/admin/admin/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
module.exports = {
import type { JestConfigWithTsJest } from "ts-jest";

const jestConfig: JestConfigWithTsJest = {
preset: "ts-jest",
testEnvironment: "jsdom",
testPathIgnorePatterns: ["/node_modules/", "/lib/"],
setupFilesAfterEnv: ["<rootDir>/setupTests.ts"],
transform: {
"^.+\\.tsx?$": [
"ts-jest",
{
tsconfig: "tsconfig.test.json",
},
],
},
// See https://testing-library.com/docs/react-testing-library/setup/#configuring-jest-with-test-utils
moduleDirectories: ["node_modules", "utils", __dirname],
};

module.exports = jestConfig;
2 changes: 2 additions & 0 deletions packages/admin/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@
"@mui/styles": "^5.0.0",
"@mui/system": "^5.0.0",
"@mui/x-data-grid": "^5.0.0",
"@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.5.1",
"@types/debounce": "^1.2.0",
"@types/file-saver": "^2.0.1",
"@types/final-form-set-field-data": "^1.0.0",
Expand Down
89 changes: 89 additions & 0 deletions packages/admin/admin/src/EditDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Button } from "@mui/material";
import userEvent from "@testing-library/user-event";
import * as React from "react";
import { render, screen, waitFor } from "test-utils";

import { useEditDialog } from "./EditDialog";
import { FinalForm } from "./FinalForm";
import { Field } from "./form/Field";
import { FinalFormInput } from "./form/FinalFormInput";

describe("EditDialog", () => {
describe("onAfterSave", () => {
test("Should call onAfterSave when clicking save button", async () => {
const user = userEvent.setup();
const onAfterSave = jest.fn();

function Story() {
const [EditDialog, , editDialogApi] = useEditDialog();

return (
<>
<Button onClick={() => editDialogApi.openAddDialog()}>Open dialog</Button>
<EditDialog onAfterSave={onAfterSave}>
<FinalForm
mode="edit"
onSubmit={async () => {
// noop
}}
>
<Field name="name" component={FinalFormInput} autoFocus />
</FinalForm>
</EditDialog>
</>
);
}

const rendered = render(<Story />);

user.click(rendered.getByText("Open dialog"));

// Edit dialog title -> dialog is open
await rendered.findByText("Add");

user.keyboard("Test");

user.click(screen.getByText("Save"));

await waitFor(() => expect(onAfterSave).toHaveBeenCalledTimes(1));
});

test("Should call onAfterSave when pressing Enter", async () => {
const user = userEvent.setup();
const onAfterSave = jest.fn();

function Story() {
const [EditDialog, , editDialogApi] = useEditDialog();

return (
<>
<Button onClick={() => editDialogApi.openAddDialog()}>Open dialog</Button>
<EditDialog onAfterSave={onAfterSave}>
<FinalForm
mode="edit"
onSubmit={async () => {
// noop
}}
>
<Field name="name" component={FinalFormInput} autoFocus />
</FinalForm>
</EditDialog>
</>
);
}

const rendered = render(<Story />);

user.click(rendered.getByText("Open dialog"));

// Edit dialog title -> dialog is open
await rendered.findByText("Add");

user.keyboard("Test");

user.keyboard("{Enter}");

await waitFor(() => expect(onAfterSave).toHaveBeenCalledTimes(1));
});
});
});
3 changes: 1 addition & 2 deletions packages/admin/admin/src/EditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function useEditDialog(): [React.ComponentType<EditDialogProps>, { id?: s
return (props: EditDialogProps) => {
return (
<Selection>
<EditDialogFormApiProvider>
<EditDialogFormApiProvider onAfterSave={props.onAfterSave}>
<EditDialogInner {...props} selection={selection} selectionApi={selectionApi} api={api} />
</EditDialogFormApiProvider>
</Selection>
Expand Down Expand Up @@ -138,7 +138,6 @@ const EditDialogInner: React.FunctionComponent<EditDialogProps & IHookProps> = (
if (!disableCloseAfterSave) {
api.closeDialog({ delay: true });
}
onAfterSave?.();
});
}
};
Expand Down
10 changes: 8 additions & 2 deletions packages/admin/admin/src/EditDialogFormApiContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@ export interface EditDialogFormApi {
hasErrors: boolean;
onFormStatusChange: (status: FormStatus) => void;
resetFormStatus: () => void;
onAfterSave?: () => void;
}

export const EditDialogFormApiContext = React.createContext<EditDialogFormApi | null>(null);
export function useEditDialogFormApi() {
return React.useContext(EditDialogFormApiContext);
}

export const EditDialogFormApiProvider: React.FunctionComponent = ({ children }) => {
type EditDialogFormApiProviderProps = {
onAfterSave?: () => void;
};

export const EditDialogFormApiProvider: React.FunctionComponent<EditDialogFormApiProviderProps> = ({ children, onAfterSave }) => {
const [status, setStatus] = React.useState<FormStatus | null>(null);

const onFormStatusChange = React.useCallback((status: FormStatus) => {
Expand All @@ -31,8 +36,9 @@ export const EditDialogFormApiProvider: React.FunctionComponent = ({ children })
hasErrors: status === "error",
onFormStatusChange,
resetFormStatus,
onAfterSave,
};
}, [onFormStatusChange, resetFormStatus, status]);
}, [onFormStatusChange, resetFormStatus, status, onAfterSave]);

return <EditDialogFormApiContext.Provider value={editDialogFormApi}>{children}</EditDialogFormApiContext.Provider>;
};
1 change: 1 addition & 0 deletions packages/admin/admin/src/FinalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export function FinalForm<FormValues = AnyObject>(props: IProps<FormValues>) {
}

onAfterSubmit(values, form);
editDialogFormApi?.onAfterSave?.();
});
return data;
})
Expand Down
28 changes: 28 additions & 0 deletions packages/admin/admin/src/utils/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Inspired by https://testing-library.com/docs/react-testing-library/setup/#custom-render
import { createTheme } from "@mui/material";
import { render, RenderOptions, RenderResult } from "@testing-library/react";
import React from "react";
import { IntlProvider } from "react-intl";

import { MuiThemeProvider } from "../mui/ThemeProvider";
import { RouterMemoryRouter } from "../router/MemoryRouter";

const messages = {};
const theme = createTheme();

function DefaultWrapper({ children }: { children: React.ReactNode }) {
return (
<IntlProvider locale="en" messages={messages}>
<MuiThemeProvider theme={theme}>
<RouterMemoryRouter>{children}</RouterMemoryRouter>
</MuiThemeProvider>
</IntlProvider>
);
}

function customRender(ui: React.ReactElement, options?: Omit<RenderOptions, "queries">): RenderResult {
return render(ui, { wrapper: DefaultWrapper, ...options });
}

export * from "@testing-library/react";
export { customRender as render };
7 changes: 6 additions & 1 deletion packages/admin/admin/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
"rootDir": "src",
"baseUrl": "src",
"paths": {
// See https://testing-library.com/docs/react-testing-library/setup/#configuring-jest-with-test-utils
"test-utils": ["./utils/test-utils"]
}
},
"include": ["./src"],
"exclude": []
Expand Down
Loading

0 comments on commit e33cd65

Please sign in to comment.