diff --git a/src/components/forms/attendee-form/__tests__/attendee-form.test.js b/src/components/forms/attendee-form/__tests__/attendee-form.test.js
new file mode 100644
index 000000000..46cc6d22d
--- /dev/null
+++ b/src/components/forms/attendee-form/__tests__/attendee-form.test.js
@@ -0,0 +1,109 @@
+import React from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import AttendeeForm from "../attendee-form";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/inputs/member-input",
+ () => ({ __esModule: true, default: () => null })
+);
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/inputs/attendee-input",
+ () => ({ __esModule: true, default: () => null })
+);
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/inputs/tag-input",
+ () => ({ __esModule: true, default: () => null })
+);
+
+jest.mock("../../../notes/notes-panel", () => ({
+ __esModule: true,
+ default: () => null
+}));
+
+const SUMMIT = { id: 1, time_zone_id: "UTC" };
+
+const defaultEntity = {
+ id: 1,
+ first_name: "Jane",
+ last_name: "Doe",
+ email: "jane@example.com",
+ company: "Acme",
+ shared_contact_info: false,
+ summit_hall_checked_in: false,
+ disclaimer_accepted: false,
+ admin_notes: "",
+ tags: [],
+ tickets: [],
+ orders: [],
+ allowed_extra_questions: []
+};
+
+const renderForm = (entityOverride = {}, { onSubmit } = {}) =>
+ render(
+
+ );
+
+describe("AttendeeForm", () => {
+ it("blocks submit and shows required-field errors when no member and fields are blank", async () => {
+ const onSubmit = jest.fn();
+ renderForm({ first_name: "", last_name: "", email: "" }, { onSubmit });
+
+ await userEvent.click(screen.getByRole("button", { name: "general.save" }));
+
+ await waitFor(() =>
+ expect(screen.getAllByText("This field is required")).toHaveLength(3)
+ );
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
+ it("submits a removeUnchangedFields-filtered payload when a field changes", async () => {
+ const onSubmit = jest.fn();
+ renderForm({}, { onSubmit });
+
+ const firstNameInput = screen.getByDisplayValue("Jane");
+ await userEvent.clear(firstNameInput);
+ await userEvent.type(firstNameInput, "Janet");
+
+ await userEvent.click(screen.getByRole("button", { name: "general.save" }));
+
+ await waitFor(() => expect(onSubmit).toHaveBeenCalled());
+ const payload = onSubmit.mock.calls[0][0];
+
+ expect(payload.first_name).toBe("Janet");
+ expect(payload).not.toHaveProperty("last_name");
+ expect(payload).not.toHaveProperty("email");
+ expect(payload).not.toHaveProperty("company");
+ expect(payload).not.toHaveProperty("shared_contact_info");
+ });
+
+ it("resyncs displayed fields when the entity prop changes", () => {
+ const { rerender } = renderForm({ first_name: "Jane" });
+ expect(screen.getByDisplayValue("Jane")).toBeInTheDocument();
+
+ rerender(
+
+ );
+
+ expect(screen.getByDisplayValue("Alice")).toBeInTheDocument();
+ expect(screen.queryByDisplayValue("Jane")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/forms/attendee-form/attendee-form.js b/src/components/forms/attendee-form/attendee-form.js
index 6c672ce5c..4e7fb8c90 100644
--- a/src/components/forms/attendee-form/attendee-form.js
+++ b/src/components/forms/attendee-form/attendee-form.js
@@ -11,14 +11,14 @@
* limitations under the License.
* */
-import React from "react";
+import React, { useEffect, useRef, useState } from "react";
import T from "i18n-react/dist/i18n-react";
import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css";
-import MemberInput from "openstack-uicore-foundation/lib/components/inputs/member-input"
-import AttendeeInput from "openstack-uicore-foundation/lib/components/inputs/attendee-input"
-import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"
-import Panel from "openstack-uicore-foundation/lib/components/sections/panel"
-import TagInput from "openstack-uicore-foundation/lib/components/inputs/tag-input"
+import MemberInput from "openstack-uicore-foundation/lib/components/inputs/member-input";
+import AttendeeInput from "openstack-uicore-foundation/lib/components/inputs/attendee-input";
+import Input from "openstack-uicore-foundation/lib/components/inputs/text-input";
+import Panel from "openstack-uicore-foundation/lib/components/sections/panel";
+import TagInput from "openstack-uicore-foundation/lib/components/inputs/tag-input";
import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown";
import ExtraQuestionsForm from "openstack-uicore-foundation/lib/components/extra-questions";
import QuestionsSet from "openstack-uicore-foundation/lib/utils/questions-set";
@@ -26,59 +26,84 @@ import TicketComponent from "./ticket-component";
import OrderComponent from "./order-component";
import RsvpComponent from "./rsvp-component";
import { AffiliationsTable } from "../../tables/affiliationstable";
-import { isEmpty, scrollToError, shallowEqual } from "../../../utils/methods";
-import Notes from "../../notes";
+import { scrollToError, shallowEqual } from "../../../utils/methods";
+import NotesPanel from "../../notes/notes-panel";
import CopyClipboard from "../../buttons/copy-clipboard";
import { MILLISECONDS_IN_SECOND } from "../../../utils/constants";
-class AttendeeForm extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- entity: { ...props.entity },
- originalEntity: { ...props.entity },
- errors: props.errors,
- showSection: "main"
- };
-
- this.formRef = React.createRef();
-
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.triggerFormSubmit = this.triggerFormSubmit.bind(this);
- this.removeUnchangedFields = this.removeUnchangedFields.bind(this);
- }
-
- componentDidUpdate(prevProps) {
- const state = {};
- scrollToError(this.props.errors);
-
- if (!shallowEqual(prevProps.entity, this.props.entity)) {
- state.entity = { ...this.props.entity };
- state.originalEntity = { ...this.props.entity };
- state.errors = {};
+const removeUnchangedFields = (entity, originalEntity) => {
+ const copyOfEntity = { ...entity };
+
+ const fields = [
+ "summit_hall_checked_in",
+ "disclaimer_accepted",
+ "has_virtual_check_in",
+ "first_name",
+ "last_name",
+ "company",
+ "shared_contact_info",
+ "admin_notes",
+ "email"
+ ];
+
+ fields.forEach((f) => {
+ if (copyOfEntity[f] === originalEntity[f]) {
+ // field dint change , so remove it from submit
+ delete copyOfEntity[f];
}
-
- if (!shallowEqual(prevProps.errors, this.props.errors)) {
- state.errors = { ...this.props.errors };
+ });
+
+ return copyOfEntity;
+};
+
+const AttendeeForm = ({
+ entity: entityProp,
+ errors: errorsProp,
+ currentSummit,
+ history,
+ onSubmit,
+ onTicketReassign,
+ onSaveTicket,
+ onDeleteRsvp,
+ ExtraQuestionsFormReadOnly
+}) => {
+ const [entity, setEntity] = useState({ ...entityProp });
+ const [originalEntity, setOriginalEntity] = useState({ ...entityProp });
+ const [errors, setErrors] = useState(errorsProp);
+ const [openSections, setOpenSections] = useState({
+ admin_notes: false,
+ extra_questions: false
+ });
+ const formRef = useRef(null);
+
+ useEffect(() => {
+ if (!shallowEqual(originalEntity, entityProp)) {
+ setEntity({ ...entityProp });
+ setOriginalEntity({ ...entityProp });
+ setErrors({});
}
+ }, [entityProp]);
- if (!isEmpty(state)) {
- this.setState({ ...this.state, ...state });
- }
- }
+ useEffect(() => {
+ scrollToError(errorsProp);
+ setErrors((prevErrors) =>
+ shallowEqual(prevErrors, errorsProp) ? prevErrors : { ...errorsProp }
+ );
+ }, [errorsProp]);
- toggleSection(section, ev) {
- const { showSection } = this.state;
- const newShowSection = showSection === section ? "main" : section;
- ev.preventDefault();
+ const toggleSection = (section, ev) => {
+ ev?.preventDefault();
+ setOpenSections((prev) => ({ ...prev, [section]: !prev[section] }));
+ };
- this.setState({ showSection: newShowSection });
- }
+ const handleOpenNotes = () => {
+ setOpenSections((prev) =>
+ prev.admin_notes ? prev : { ...prev, admin_notes: true }
+ );
+ };
- handleChange(ev) {
- const entity = { ...this.state.entity };
+ const handleChange = (ev) => {
+ const updatedEntity = { ...entity };
let { value, id } = ev.target;
if (ev.target.type === "checkbox") {
@@ -94,402 +119,355 @@ class AttendeeForm extends React.Component {
}
if (id === "member") {
- entity.email = value?.email || "";
- entity.first_name = value?.first_name || "";
- entity.last_name = value?.last_name || "";
+ updatedEntity.email = value?.email || "";
+ updatedEntity.first_name = value?.first_name || "";
+ updatedEntity.last_name = value?.last_name || "";
}
- entity[id] = value;
- this.setState({ entity, errors: {} });
- }
-
- removeUnchangedFields(entity, originalEntity) {
- const copyOfEntity = { ...entity };
-
- const fields = [
- "summit_hall_checked_in",
- "disclaimer_accepted",
- "has_virtual_check_in",
- "first_name",
- "last_name",
- "company",
- "shared_contact_info",
- "admin_notes",
- "email"
- ];
-
- fields.forEach((f) => {
- if (copyOfEntity[f] === originalEntity[f]) {
- // field dint change , so remove it from submit
- delete copyOfEntity[f];
- }
- });
+ updatedEntity[id] = value;
+ setEntity(updatedEntity);
+ setErrors({});
+ };
- return copyOfEntity;
- }
+ const validate = () => {
+ const newErrors = { ...errors };
+ const required = ["first_name", "last_name", "email"];
- triggerFormSubmit() {
- if (!this.validate()) return false;
+ if (!entity.member) {
+ required.forEach((fieldId) => {
+ if (!entity[fieldId]) {
+ newErrors[fieldId] = "This field is required";
+ }
+ });
+ }
+
+ if (Object.keys(newErrors).length > 0) {
+ setErrors(newErrors);
+ return false;
+ }
+
+ return true;
+ };
+
+ const triggerFormSubmit = () => {
+ if (!validate()) return false;
// check current ( could not be rendered)
- if (this.formRef.current) {
- this.formRef.current.doSubmit();
+ if (formRef.current) {
+ formRef.current.doSubmit();
return true;
}
// do regular submit
-
- const { originalEntity, entity } = this.state;
-
- if (entity.extra_questions) {
- entity.extra_questions = entity.extra_questions.map((q) => ({
- question_id: q.question_id,
- answer: q.value
- }));
+ const updatedEntity = { ...entity };
+
+ if (updatedEntity.extra_questions) {
+ updatedEntity.extra_questions = updatedEntity.extra_questions.map(
+ (q) => ({
+ question_id: q.question_id,
+ answer: q.value
+ })
+ );
}
- this.props.onSubmit(this.removeUnchangedFields(entity, originalEntity));
+ onSubmit(removeUnchangedFields(updatedEntity, originalEntity));
return true;
- }
-
- handleSubmit(formValues) {
- const qs = new QuestionsSet(this.state.entity.allowed_extra_questions);
- const formattedAnswers = [];
+ };
- Object.keys(formValues).map((name) => {
+ const handleSubmit = (formValues) => {
+ const qs = new QuestionsSet(entity.allowed_extra_questions);
+ const formattedAnswers = Object.keys(formValues).map((name) => {
const question = qs.getQuestionByName(name);
- const newQuestion = {
+ return {
question_id: question.id,
answer: `${formValues[name]}`
};
- formattedAnswers.push(newQuestion);
});
- this.setState(
- {
- ...this.state,
- entity: { ...this.state.entity, extra_questions: formattedAnswers }
- },
- () => {
- const { originalEntity, entity } = this.state;
- this.props.onSubmit(this.removeUnchangedFields(entity, originalEntity));
- }
- );
- }
+ const updatedEntity = { ...entity, extra_questions: formattedAnswers };
+ setEntity(updatedEntity);
+ onSubmit(removeUnchangedFields(updatedEntity, originalEntity));
+ };
- handleSpeakerLink(speaker_id, ev) {
- const { history } = this.props;
+ const handleSpeakerLink = (speaker_id, ev) => {
ev.preventDefault();
-
history.push(`/app/speakers/${speaker_id}`);
- }
+ };
- hasErrors(field) {
- const { errors } = this.state;
+ const hasErrors = (field) => {
if (field in errors) {
return errors[field];
}
return "";
- }
-
- handleNewTag(newTag) {
- this.setState({
- ...this.state,
- entity: {
- ...this.state.entity,
- tags: [...this.state.entity.tags, { tag: newTag }]
- }
- });
- }
-
- validate = () => {
- const { entity, errors } = this.state;
- const required = ["first_name", "last_name", "email"];
-
- if (!entity.member) {
- required.forEach((fieldId) => {
- if (!entity[fieldId]) {
- errors[fieldId] = "This field is required";
- }
- });
- }
-
- if (Object.keys(errors).length > 0) {
- this.setState({ errors });
- return false;
- }
+ };
- return true;
+ const handleNewTag = (newTag) => {
+ setEntity({
+ ...entity,
+ tags: [...entity.tags, { tag: newTag }]
+ });
};
- render() {
- const { entity, showSection } = this.state;
- const { currentSummit } = this.props;
- const disableMemberInput = !entity.member && entity.email;
-
- return (
- <>
- {/* First Form ( Main Attendee Form ) */}
-