Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/components/forms/__tests__/badge-settings-form.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import BadgeSettingsForm from "../badge-settings-form";

jest.mock("i18n-react/dist/i18n-react", () => ({
__esModule: true,
default: { translate: (key) => key }
}));

jest.mock("sweetalert2", () => ({
__esModule: true,
default: { fire: jest.fn() }
}));

const mockSummit = { id: 1, badge_features_types: [], badge_types: [] };

const renderForm = (onSubmit) =>
render(
<BadgeSettingsForm
entity={{}}
currentSummit={mockSummit}
errors={{}}
onSubmit={onSubmit}
onDeleteImage={jest.fn()}
onDeleteBadgeTypeImage={jest.fn()}
/>
);

it("should call onSubmit only once when Save is clicked twice while saving", async () => {
const pendingPromise = new Promise(() => {});
const onSubmit = jest.fn(() => pendingPromise);
const { container } = renderForm(onSubmit);

fireEvent.change(container.querySelector("#BADGE_TEMPLATE_WIDTH"), {
target: { value: "100" }
});

const saveButton = screen.getByRole("button", { name: "general.save" });
fireEvent.click(saveButton);
fireEvent.click(saveButton);

expect(onSubmit).toHaveBeenCalledTimes(1);
});

it("should re-enable Save and not throw an unhandled rejection when onSubmit rejects", async () => {
const onSubmit = jest.fn(() => Promise.reject(new Error("412")));
const { container } = renderForm(onSubmit);

fireEvent.change(container.querySelector("#BADGE_TEMPLATE_WIDTH"), {
target: { value: "100" }
});

fireEvent.click(screen.getByRole("button", { name: "general.save" }));

await waitFor(() => {
expect(
screen.getByRole("button", { name: "general.save" })
).not.toBeDisabled();
});
});
40 changes: 26 additions & 14 deletions src/components/forms/badge-settings-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
* */
import React from "react";
import T from "i18n-react/dist/i18n-react";
import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input"
import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"
import TextArea from "openstack-uicore-foundation/lib/components/inputs/textarea-input"
import Panel from "openstack-uicore-foundation/lib/components/sections/panel"
import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input";
import Input from "openstack-uicore-foundation/lib/components/inputs/text-input";
import TextArea from "openstack-uicore-foundation/lib/components/inputs/textarea-input";
import Panel from "openstack-uicore-foundation/lib/components/sections/panel";
import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown";
import Switch from "react-switch";
import Swal from "sweetalert2";
Expand All @@ -30,7 +30,8 @@ class BadgeSettingsForm extends React.Component {
this.state = {
entity: { ...props.entity },
errors: props.errors,
showSection: null
showSection: null,
isSaving: false
};

this.handleChange = this.handleChange.bind(this);
Expand Down Expand Up @@ -159,26 +160,36 @@ class BadgeSettingsForm extends React.Component {
handleSubmit(ev) {
ev.preventDefault();

if (this.state.isSaving) return;

// save only the settings with the following conditions
const settingsToSave = Object.fromEntries(
Object.entries(this.state.entity).filter(
([, values]) => values.updated === true
)
);

this.props.onSubmit(settingsToSave).then(() => {
const success_message = {
title: T.translate("general.done"),
html: T.translate("badge_settings.badge_template_settings_updated"),
type: "success"
};
this.setState({ isSaving: true });

Swal.fire(success_message);
});
this.props
.onSubmit(settingsToSave)
.then(() => {
const success_message = {
title: T.translate("general.done"),
html: T.translate("badge_settings.badge_template_settings_updated"),
type: "success"
};

Swal.fire(success_message);
})
.catch(() => {})
.finally(() => {
this.setState({ isSaving: false });
});
Comment on lines +172 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file outline ---'
ast-grep outline src/components/forms/badge-settings-form.js --view expanded || true

echo '--- relevant slice ---'
sed -n '140,220p' src/components/forms/badge-settings-form.js

echo '--- search onSubmit usages ---'
rg -n "onSubmit\s*[:=]" src -g '!**/node_modules/**' || true
rg -n "<BadgeSettingsForm|badge-settings-form|onSubmit=" src -g '!**/node_modules/**' || true

Repository: fntechgit/summit-admin

Length of output: 42278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## badge-settings-form.js excerpt\n'
sed -n '1,260p' src/components/forms/badge-settings-form.js

printf '\n## BadgeSettingsForm usages\n'
rg -n "BadgeSettingsForm|badge-settings-form|onSubmit=" src

printf '\n## onSubmit prop types / docs in surrounding components\n'
rg -n "onSubmit.*PropTypes|PropTypes.*onSubmit|onSubmit:" src/components src/pages src -g '!**/node_modules/**'

Repository: fntechgit/summit-admin

Length of output: 33762


Use a Promise boundary around onSubmit
handleSubmit still depends on onSubmit(settingsToSave) returning a thenable. If a caller throws synchronously or returns a non-Promise, isSaving can remain stuck true. Wrapping the call with Promise.resolve().then(() => this.props.onSubmit(settingsToSave)) makes the save state recoverable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/forms/badge-settings-form.js` around lines 172 - 188, Update
handleSubmit’s onSubmit flow to create a Promise boundary with
Promise.resolve().then(() => this.props.onSubmit(settingsToSave)), preserving
the existing success, catch, and finally handlers so synchronous throws or
non-Promise returns always reset isSaving.

}

render() {
const { entity, showSection } = this.state;
const { entity, showSection, isSaving } = this.state;
const { currentSummit } = this.props;

const ddlAlignOptions = [
Expand Down Expand Up @@ -1580,6 +1591,7 @@ class BadgeSettingsForm extends React.Component {
onClick={this.handleSubmit}
className="btn btn-primary pull-right"
value={T.translate("general.save")}
disabled={isSaving}
/>
</div>
</div>
Expand Down
Loading