Skip to content

Commit

Permalink
Edit Profile Form (thewca#9811)
Browse files Browse the repository at this point in the history
* Edit Profile Form

* Review changes

* Fixed unit test issue

* Removed old section of edit profile form

* Review changes
  • Loading branch information
danieljames-dj authored Nov 18, 2024
1 parent c629fc7 commit 4c00e1c
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 163 deletions.
40 changes: 39 additions & 1 deletion app/controllers/contacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ContactsController < ApplicationController
end

private def new_profile_data_key_to_value(new_profile_data, profile_data_to_change)
if profile_data_to_change == 'country'
if profile_data_to_change == 'country_iso2'
Country.find_by(iso2: new_profile_data).name
else
new_profile_data
Expand Down Expand Up @@ -99,6 +99,44 @@ def contact
end
end

def edit_profile_action
formValues = JSON.parse(params.require(:formValues), symbolize_names: true)
edited_profile_details = formValues[:editedProfileDetails]
edit_profile_reason = formValues[:editProfileReason]
attachment = params[:attachment]
wca_id = formValues[:wcaId]

return render status: :unauthorized, json: { error: "Cannot request profile change without login" } unless current_user.present?

profile_to_edit = {
name: current_user.person.name,
country_iso2: current_user.person.country_iso2,
gender: current_user.person.gender,
dob: current_user.person.dob,
}
changes_requested = Person.fields_edit_requestable
.reject { |field| profile_to_edit[field].to_s == edited_profile_details[field].to_s }
.map { |field|
ContactEditProfile::EditProfileChange.new(
field: field.to_s.humanize,
from: (new_profile_data_key_to_value(profile_to_edit[field], field.to_s) || "Unknown").to_s,
to: new_profile_data_key_to_value(edited_profile_details[field], field.to_s),
)
}

maybe_send_contact_email(
ContactEditProfile.new(
your_email: current_user&.email,
name: profile_to_edit[:name],
wca_id: wca_id,
changes_requested: changes_requested,
edit_profile_reason: edit_profile_reason,
document: attachment,
request: request,
),
)
end

def dob
@contact = DobContact.new(your_email: current_user&.email)
end
Expand Down
26 changes: 26 additions & 0 deletions app/models/contact_edit_profile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

class ContactEditProfile < ContactForm
attribute :wca_id
attribute :changes_requested
attribute :edit_profile_reason
attribute :document, attachment: true

EditProfileChange = Struct.new(
:field,
:from,
:to,
)

def to_email
UserGroup.teams_committees_group_wrt.metadata.email
end

def subject
Time.now.strftime("Edit Profile request by #{wca_id} on %d %b %Y at %R")
end

def headers
super.merge(template_name: "contact_edit_profile")
end
end
4 changes: 4 additions & 0 deletions app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ def url
Rails.application.routes.url_helpers.person_url(wca_id, host: EnvConfig.ROOT_URL)
end

def self.fields_edit_requestable
[:name, :country_iso2, :gender, :dob].freeze
end

DEFAULT_SERIALIZE_OPTIONS = {
only: ["wca_id", "name", "gender"],
methods: ["url", "country_iso2"],
Expand Down
5 changes: 5 additions & 0 deletions app/views/contacts/edit_profile.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<% provide(:title, t('page.contact_edit_profile.title')) %>
<%= react_component("ContactEditProfilePage", {
loggedInUserId: current_user&.id,
recaptchaPublicKey: AppSecrets.RECAPTCHA_PUBLIC_KEY,
}) %>
21 changes: 21 additions & 0 deletions app/views/mail_form/contact_edit_profile.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<p>Hey WRT,</p>
<p>
<%= @resource.wca_id %> requested following change in their profile:
</p>
<table>
<body>
<% @resource.changes_requested.each do |change| %>
<tr>
<td><%= change[:field] %></td>
<td><%= change[:from] %> -> <%= change[:to] %></td>
</tr>
<% end %>
<tr style="height: 1em">
<td colspan="2"></td>
</tr>
</body>
</table>
<p>You can edit this person <%= link_to "here", panel_index_url(panel_id: 'wrt', wcaId: @resource.wca_id, anchor: User.panel_pages[:editPerson]) %>.</p>
<% if @resource.document.present? %>
<p>Note: There is a proof attachment to this email.</p>
<% end %>
163 changes: 163 additions & 0 deletions app/webpacker/components/ContactEditProfilePage/EditProfileForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Form, Message } from 'semantic-ui-react';
import ReCAPTCHA from 'react-google-recaptcha';
import { QueryClient, useQuery } from '@tanstack/react-query';
import i18n from '../../lib/i18n';
import { apiV0Urls, contactEditProfileActionUrl } from '../../lib/requests/routes.js.erb';
import { genders, countries } from '../../lib/wca-data.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';

const CONTACT_EDIT_PROFILE_FORM_QUERY_CLIENT = new QueryClient();

const genderOptions = _.map(genders.byId, (gender) => ({
key: gender.id,
text: gender.name,
value: gender.id,
}));

const countryOptions = _.map(countries.byIso2, (country) => ({
key: country.iso2,
text: country.name,
value: country.iso2,
}));

export default function EditProfileForm({
wcaId,
onContactSuccess,
recaptchaPublicKey,
}) {
const [editProfileReason, setEditProfileReason] = useState();
const [editedProfileDetails, setEditedProfileDetails] = useState();
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 profileDetails = data?.data?.person;

const isSubmitDisabled = useMemo(
() => !editedProfileDetails || _.isEqual(editedProfileDetails, profileDetails) || !captchaValue,
[captchaValue, editedProfileDetails, profileDetails],
);

useEffect(() => {
setEditedProfileDetails(profileDetails);
}, [profileDetails]);

const formSubmitHandler = () => {
const formData = new FormData();

formData.append('formValues', JSON.stringify({
editedProfileDetails, editProfileReason, wcaId,
}));
formData.append('attachment', proofAttachment);

save(
contactEditProfileActionUrl,
formData,
onContactSuccess,
{ method: 'POST', headers: {}, body: formData },
setSaveError,
);
};

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 <Loading />;
if (saveError || isError) return <Errored />;

return (
<Form onSubmit={formSubmitHandler}>
<Form.Input
label={i18n.t('activerecord.attributes.user.name')}
name="name"
value={editedProfileDetails?.name}
onChange={handleFormChange}
/>
<Form.Select
options={countryOptions}
label={i18n.t('activerecord.attributes.user.country_iso2')}
name="country_iso2"
search
value={editedProfileDetails?.country_iso2}
onChange={handleFormChange}
/>
<Form.Select
options={genderOptions}
label={i18n.t('activerecord.attributes.user.gender')}
name="gender"
value={editedProfileDetails?.gender}
onChange={handleFormChange}
/>
<Form.Field
label={i18n.t('activerecord.attributes.user.dob')}
name="dob"
control={UtcDatePicker}
showYearDropdown
dateFormatOverride="YYYY-MM-dd"
dropdownMode="select"
isoDate={editedProfileDetails?.dob}
onChange={handleDobChange}
/>
<Form.TextArea
label={i18n.t('page.contact_edit_profile.form.edit_reason.label')}
name="editProfileReason"
required
value={editProfileReason}
onChange={handleEditProfileReasonChange}
/>
<Form.Input
label={i18n.t('page.contact_edit_profile.form.proof_attach.label')}
type="file"
onChange={handleProofUpload}
/>
<Form.Field>
<ReCAPTCHA
sitekey={recaptchaPublicKey}
// onChange is a mandatory parameter for ReCAPTCHA. According to the documentation, this
// is called when user successfully completes the captcha, hence we are assuming that any
// existing errors will be cleared when onChange is called.
onChange={setCaptchaValue}
onErrored={setCaptchaError}
/>
{captchaError && (
<Message
error
content={i18n.t('page.contact_edit_profile.form.captcha.validation_error')}
/>
)}
</Form.Field>
<Form.Button
type="submit"
disabled={isSubmitDisabled}
>
{i18n.t('page.contact_edit_profile.form.submit_edit_request_button.label')}
</Form.Button>
</Form>
);
}
58 changes: 58 additions & 0 deletions app/webpacker/components/ContactEditProfilePage/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { Container, Header, Message } from 'semantic-ui-react';
import { QueryClient, useQuery } from '@tanstack/react-query';
import i18n from '../../lib/i18n';
import I18nHTMLTranslate from '../I18nHTMLTranslate';
import { apiV0Urls } 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';

const CONTACT_EDIT_PROFILE_QUERY_CLIENT = new QueryClient();

export default function ContactEditProfilePage({ loggedInUserId, recaptchaPublicKey }) {
const { data: loggedInUserData, isLoading, isError } = useQuery({
queryKey: ['userData'],
queryFn: () => fetchJsonOrError(apiV0Urls.users.me.userDetails),
enabled: !!loggedInUserId,
}, CONTACT_EDIT_PROFILE_QUERY_CLIENT);
const wcaId = loggedInUserData?.data?.user?.wca_id;
const [contactSuccess, setContactSuccess] = useState(false);

if (isLoading) return <Loading />;
if (isError) return <Errored />;
if (!loggedInUserData) {
return (
<Message error>
<I18nHTMLTranslate i18nKey="page.contact_edit_profile.not_logged_in_error" />
</Message>
);
}
if (loggedInUserData && !wcaId) {
return (
<Message error>
<I18nHTMLTranslate i18nKey="page.contact_edit_profile.no_profile_error" />
</Message>
);
}
if (contactSuccess) {
return (
<Message
success
content={i18n.t('page.contact_edit_profile.success_message')}
/>
);
}

return (
<Container text>
<Header as="h2">{i18n.t('page.contact_edit_profile.title')}</Header>
<EditProfileForm
wcaId={wcaId}
onContactSuccess={() => setContactSuccess(true)}
recaptchaPublicKey={recaptchaPublicKey}
/>
</Container>
);
}
Loading

0 comments on commit 4c00e1c

Please sign in to comment.