From 92a822a9a1d7a4793ce5790e4558df1180795b21 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Tue, 12 May 2026 21:01:07 +0530 Subject: [PATCH 1/3] Modify edit profile form by requiring edit reason for each field --- app/controllers/contacts_controller.rb | 11 +- .../EditProfileForm.jsx | 135 ++++++++---------- .../EditProfileFormHolder.jsx | 32 +++++ .../fields/EditDobField.jsx | 32 +++++ .../fields/EditGenderField.jsx | 25 ++++ .../fields/EditNameField.jsx | 27 ++++ .../fields/EditReasonField.jsx | 25 ++++ .../fields/EditRegionField.jsx | 27 ++++ .../ContactEditProfilePage/index.jsx | 4 +- config/locales/en.yml | 2 + 10 files changed, 240 insertions(+), 80 deletions(-) create mode 100644 app/webpacker/components/ContactEditProfilePage/EditProfileFormHolder.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/fields/EditDobField.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/fields/EditGenderField.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/fields/EditNameField.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/fields/EditReasonField.jsx create mode 100644 app/webpacker/components/ContactEditProfilePage/fields/EditRegionField.jsx diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index b94c372048..49e9ec48ad 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -95,8 +95,7 @@ def contact def edit_profile_action form_values = JSON.parse(params.require(:formValues), symbolize_names: true) - edited_profile_details = form_values[:editedProfileDetails] - edit_profile_reason = form_values[:editProfileReason] + edited_profile_details_from_form = form_values[:editedProfileDetails] attachment = params[:attachment] wca_id = form_values[:wcaId] person = Person.find_by(wca_id: wca_id) @@ -114,6 +113,14 @@ def edit_profile_action gender: person.gender, dob: person.dob, } + + edited_profile_details = edited_profile_details_from_form.transform_values { |v| v[:newValue] } + edit_profile_reason = edited_profile_details_from_form.filter_map do |field, v| + if profile_to_edit[field].to_s != v[:newValue].to_s + "#{I18n.t("activerecord.attributes.user.#{field}")}: #{v[:editReason]}" + end + end.join("\n") + changes_requested = Person.fields_edit_requestable .reject { |field| profile_to_edit[field].to_s == edited_profile_details[field].to_s } .map do |field| diff --git a/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx b/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx index a37a85809a..6dec113979 100644 --- a/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx +++ b/app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx @@ -1,54 +1,78 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useMemo, useState, useCallback } from 'react'; import { Form, Message } from 'semantic-ui-react'; import ReCAPTCHA from 'react-google-recaptcha'; -import { QueryClient, useQuery } from '@tanstack/react-query'; import _ from 'lodash'; import I18n from '../../lib/i18n'; -import { apiV0Urls, contactEditProfileActionUrl } from '../../lib/requests/routes.js.erb'; +import { contactEditProfileActionUrl } from '../../lib/requests/routes.js.erb'; import Loading from '../Requests/Loading'; -import Errored from '../Requests/Errored'; import useSaveAction from '../../lib/hooks/useSaveAction'; -import { fetchJsonOrError } from '../../lib/requests/fetchWithAuthenticityToken'; -import UtcDatePicker from '../wca/UtcDatePicker'; -import RegionSelector from '../wca/RegionSelector'; -import GenderSelector from '../wca/GenderSelector'; +import EditNameField from './fields/EditNameField'; +import EditRegionField from './fields/EditRegionField'; +import EditGenderField from './fields/EditGenderField'; +import EditDobField from './fields/EditDobField'; -const CONTACT_EDIT_PROFILE_FORM_QUERY_CLIENT = new QueryClient(); +const EDITABLE_FIELDS = [ + { name: 'name', Component: EditNameField }, + { name: 'country_iso2', Component: EditRegionField }, + { name: 'gender', Component: EditGenderField }, + { name: 'dob', Component: EditDobField }, +]; export default function EditProfileForm({ wcaId, + profileDetails, onContactSuccess, recaptchaPublicKey, }) { - const [editProfileReason, setEditProfileReason] = useState(); - const [editedProfileDetails, setEditedProfileDetails] = useState(); + const [editedProfileDetails, setEditedProfileDetails] = useState(() => _.fromPairs( + EDITABLE_FIELDS.map(({ name }) => [ + name, + { newValue: profileDetails?.[name] || '', editReason: '' }, + ]), + )); const [proofAttachment, setProofAttachment] = useState(); const [captchaValue, setCaptchaValue] = useState(); const [captchaError, setCaptchaError] = useState(false); const [saveError, setSaveError] = useState(); const { save, saving } = useSaveAction(); - const { data, isLoading, isError } = useQuery({ - queryKey: ['profileData'], - queryFn: () => fetchJsonOrError(apiV0Urls.persons.show(wcaId)), - }, CONTACT_EDIT_PROFILE_FORM_QUERY_CLIENT); + const hasFieldBeenChanged = useCallback((field) => !_.isEqual( + profileDetails?.[field], + editedProfileDetails[field].newValue, + ), [profileDetails, editedProfileDetails]); - const profileDetails = data?.data?.person; + const isSubmitDisabled = useMemo(() => { + if (!profileDetails || !captchaValue) return true; - const isSubmitDisabled = useMemo( - () => !editedProfileDetails || _.isEqual(editedProfileDetails, profileDetails) || !captchaValue, - [captchaValue, editedProfileDetails, profileDetails], - ); + const changedFields = Object.keys(editedProfileDetails).filter(hasFieldBeenChanged); + + const noChanges = changedFields.length === 0; + const hasMissingReason = changedFields.some( + (field) => !editedProfileDetails[field].editReason.trim(), + ); - useEffect(() => { - setEditedProfileDetails(profileDetails); - }, [profileDetails]); + return noChanges || hasMissingReason; + }, [captchaValue, editedProfileDetails, hasFieldBeenChanged, profileDetails]); + + const handleValueChange = (_event, { name, value }) => { + setEditedProfileDetails((prev) => ({ + ...prev, + [name]: { ...prev[name], newValue: value }, + })); + }; + + const handleEditReasonChange = (_event, { name, value }) => { + setEditedProfileDetails((prev) => ({ + ...prev, + [name]: { ...prev[name], editReason: value }, + })); + }; const formSubmitHandler = () => { const formData = new FormData(); formData.append('formValues', JSON.stringify({ - editedProfileDetails, editProfileReason, wcaId, + editedProfileDetails, wcaId, })); if (proofAttachment) { formData.append('attachment', proofAttachment); @@ -63,25 +87,11 @@ export default function EditProfileForm({ ); }; - const handleEditProfileReasonChange = (e, { value }) => { - setEditProfileReason(value); - }; - const handleProofUpload = (event) => { setProofAttachment(event.target.files[0]); }; - const handleFormChange = (e, { name: formName, value }) => { - setEditedProfileDetails((prev) => ({ ...prev, [formName]: value })); - }; - - const handleDobChange = (date) => handleFormChange(null, { - name: 'dob', - value: date, - }); - - if (saving || isLoading) return ; - if (isError) return ; + if (saving) return ; return (
@@ -91,43 +101,16 @@ export default function EditProfileForm({ content={saveError.json?.error || 'Something went wrong.'} /> )} - - - - - + {EDITABLE_FIELDS.map(({ name, Component }) => ( + + ))} fetchJsonOrError(apiV0Urls.persons.show(wcaId)), + }); + + if (isLoading) return ; + if (isError) return ; + + const profileDetails = data?.data?.person; + + return ( + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/fields/EditDobField.jsx b/app/webpacker/components/ContactEditProfilePage/fields/EditDobField.jsx new file mode 100644 index 0000000000..c4956ba681 --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/fields/EditDobField.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { Form } from 'semantic-ui-react'; +import I18n from '../../../lib/i18n'; +import UtcDatePicker from '../../wca/UtcDatePicker'; +import EditReasonField from './EditReasonField'; + +export default function EditDobField({ + value, reason, isChanged, onValueChange, onReasonChange, +}) { + return ( + <> + onValueChange(null, { name: 'dob', value: date })} + required + /> + + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/fields/EditGenderField.jsx b/app/webpacker/components/ContactEditProfilePage/fields/EditGenderField.jsx new file mode 100644 index 0000000000..d40a48e807 --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/fields/EditGenderField.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import GenderSelector from '../../wca/GenderSelector'; +import EditReasonField from './EditReasonField'; +import I18n from '../../../lib/i18n'; + +export default function EditGenderField({ + value, reason, isChanged, onValueChange, onReasonChange, +}) { + return ( + <> + + + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/fields/EditNameField.jsx b/app/webpacker/components/ContactEditProfilePage/fields/EditNameField.jsx new file mode 100644 index 0000000000..4e20aef856 --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/fields/EditNameField.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Form } from 'semantic-ui-react'; +import I18n from '../../../lib/i18n'; +import EditReasonField from './EditReasonField'; + +export default function EditNameField({ + value, reason, isChanged, onValueChange, onReasonChange, +}) { + return ( + <> + + + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/fields/EditReasonField.jsx b/app/webpacker/components/ContactEditProfilePage/fields/EditReasonField.jsx new file mode 100644 index 0000000000..b55b35c907 --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/fields/EditReasonField.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Form } from 'semantic-ui-react'; +import I18n from '../../../lib/i18n'; + +export default function EditReasonField({ + name, + label, + isChanged, + value, + onChange, +}) { + if (!isChanged) return null; + + return ( + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/fields/EditRegionField.jsx b/app/webpacker/components/ContactEditProfilePage/fields/EditRegionField.jsx new file mode 100644 index 0000000000..ea69b66d4b --- /dev/null +++ b/app/webpacker/components/ContactEditProfilePage/fields/EditRegionField.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import I18n from '../../../lib/i18n'; +import RegionSelector from '../../wca/RegionSelector'; +import EditReasonField from './EditReasonField'; + +export default function EditRegionField({ + value, reason, isChanged, onValueChange, onReasonChange, +}) { + return ( + <> + + + + ); +} diff --git a/app/webpacker/components/ContactEditProfilePage/index.jsx b/app/webpacker/components/ContactEditProfilePage/index.jsx index dfa2ed2fa6..f2efce4fa5 100644 --- a/app/webpacker/components/ContactEditProfilePage/index.jsx +++ b/app/webpacker/components/ContactEditProfilePage/index.jsx @@ -7,7 +7,7 @@ import { apiV0Urls, viewUrls } from '../../lib/requests/routes.js.erb'; import Loading from '../Requests/Loading'; import { fetchJsonOrError } from '../../lib/requests/fetchWithAuthenticityToken'; import Errored from '../Requests/Errored'; -import EditProfileForm from './EditProfileForm'; +import EditProfileFormHolder from './EditProfileFormHolder'; import useLoggedInUserPermissions from '../../lib/hooks/useLoggedInUserPermissions'; import useQueryParams from '../../lib/hooks/useQueryParams'; import useInputState from '../../lib/hooks/useInputState'; @@ -102,7 +102,7 @@ function ContactEditProfilePage({ loggedInUserId, recaptchaPublicKey }) { /> )} {wcaId && ( - { setContactSuccess(true); diff --git a/config/locales/en.yml b/config/locales/en.yml index 3d147edcd1..2637e44267 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -902,6 +902,8 @@ en: label: "Search for the WCA ID that you want to edit." edit_reason: label: "Reason for the change" + edit_reason_for: + label: "Reason for %{attribute} change" proof_attach: label: "Attach proof" submit_edit_request_button: From 795208a4af358ba67556b1a14721ecec921403a2 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Tue, 12 May 2026 21:17:12 +0530 Subject: [PATCH 2/3] refactor: inline conditional logic for profile edit reason collection --- app/controllers/contacts_controller.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index 49e9ec48ad..fc57aa36d0 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -116,9 +116,7 @@ def edit_profile_action edited_profile_details = edited_profile_details_from_form.transform_values { |v| v[:newValue] } edit_profile_reason = edited_profile_details_from_form.filter_map do |field, v| - if profile_to_edit[field].to_s != v[:newValue].to_s - "#{I18n.t("activerecord.attributes.user.#{field}")}: #{v[:editReason]}" - end + "#{I18n.t("activerecord.attributes.user.#{field}")}: #{v[:editReason]}" if profile_to_edit[field].to_s != v[:newValue].to_s end.join("\n") changes_requested = Person.fields_edit_requestable From 7261ecba0177a467c197e011551a29a192199022 Mon Sep 17 00:00:00 2001 From: Daniel James Date: Wed, 13 May 2026 22:46:28 +0530 Subject: [PATCH 3/3] Add reason to EditProfileChange struct --- app/controllers/contacts_controller.rb | 5 +---- app/models/contact_edit_profile.rb | 2 +- app/views/mail_form/contact_edit_profile.erb | 8 +++++++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/controllers/contacts_controller.rb b/app/controllers/contacts_controller.rb index fc57aa36d0..13862aa0dd 100644 --- a/app/controllers/contacts_controller.rb +++ b/app/controllers/contacts_controller.rb @@ -115,9 +115,6 @@ def edit_profile_action } edited_profile_details = edited_profile_details_from_form.transform_values { |v| v[:newValue] } - edit_profile_reason = edited_profile_details_from_form.filter_map do |field, v| - "#{I18n.t("activerecord.attributes.user.#{field}")}: #{v[:editReason]}" if profile_to_edit[field].to_s != v[:newValue].to_s - end.join("\n") changes_requested = Person.fields_edit_requestable .reject { |field| profile_to_edit[field].to_s == edited_profile_details[field].to_s } @@ -126,6 +123,7 @@ def edit_profile_action field: field, from: profile_to_edit[field], to: edited_profile_details[field], + reason: edited_profile_details_from_form[field][:editReason], ) end @@ -134,7 +132,6 @@ def edit_profile_action name: profile_to_edit[:name], wca_id: wca_id, changes_requested: changes_requested, - edit_profile_reason: edit_profile_reason, requestor_user: current_user, document: attachment, request: request, diff --git a/app/models/contact_edit_profile.rb b/app/models/contact_edit_profile.rb index 813766f222..4769edd0c5 100644 --- a/app/models/contact_edit_profile.rb +++ b/app/models/contact_edit_profile.rb @@ -3,7 +3,6 @@ class ContactEditProfile < ContactForm attribute :wca_id attribute :changes_requested - attribute :edit_profile_reason attribute :requestor_user attribute :ticket attribute :document, attachment: true @@ -12,6 +11,7 @@ class ContactEditProfile < ContactForm :field, :from, :to, + :reason, ) def value_humanized(value, field) diff --git a/app/views/mail_form/contact_edit_profile.erb b/app/views/mail_form/contact_edit_profile.erb index d7b8839feb..41a7ea6f0a 100644 --- a/app/views/mail_form/contact_edit_profile.erb +++ b/app/views/mail_form/contact_edit_profile.erb @@ -9,13 +9,19 @@ <%= change[:field].to_s.humanize %> <%= @resource.value_humanized(change[:from], change[:field]) %> -> <%= @resource.value_humanized(change[:to], change[:field]) %> + <% if change[:reason].present? %> + + + Reason: <%= change[:reason] %> + + + <% end %> <% end %> -

Edit Reason: <%= @resource.edit_profile_reason %>

Requestor: <%= @resource.requestor_info %>

You can edit this person using <%= link_to "this ticket (##{@resource.ticket.id})", ticket_url(@resource.ticket.id) %>.

<% if @resource.document.present? %>