From aa432d08b3f5a340d9de0915cc77d466dbf5ceaf Mon Sep 17 00:00:00 2001 From: Sylvain Date: Tue, 26 Apr 2022 18:05:18 +0200 Subject: [PATCH] (wip)(ui) refactor user edition form --- app/controllers/confirmations_controller.rb | 8 +- app/controllers/passwords_controller.rb | 5 + .../src/javascript/api/auth-provider.ts | 7 +- .../src/javascript/api/authentication.ts | 23 ++++ .../openid-connect-form.tsx | 2 +- .../javascript/components/form/form-input.tsx | 14 +- .../components/form/form-multi-select.tsx | 4 +- .../components/form/form-select.tsx | 4 +- .../subscriptions/free-extend-modal.tsx | 3 +- .../components/user/change-password.tsx | 72 ++++++++++ .../components/user/gender-input.tsx | 33 +++++ .../components/user/password-input.tsx | 51 ++++++++ .../components/user/user-profile-form.tsx | 123 ++++++++++++++++-- .../src/javascript/controllers/members.js | 8 ++ app/frontend/src/javascript/lib/format.ts | 5 +- .../models/authentication-provider.ts | 8 ++ .../src/javascript/models/form-component.ts | 23 ++-- app/frontend/src/javascript/models/payment.ts | 3 +- app/frontend/src/javascript/models/user.ts | 2 + .../src/javascript/typings/date-iso.d.ts | 8 ++ app/frontend/src/stylesheets/application.scss | 2 + .../modules/user/gender-input.scss | 36 +++++ .../modules/user/user-profile-form.scss | 43 ++++++ .../templates/dashboard/settings.html | 2 +- config/locales/app.shared.en.yml | 40 +++--- config/routes.rb | 1 + package.json | 2 +- yarn.lock | 8 +- 28 files changed, 477 insertions(+), 63 deletions(-) create mode 100644 app/frontend/src/javascript/api/authentication.ts create mode 100644 app/frontend/src/javascript/components/user/change-password.tsx create mode 100644 app/frontend/src/javascript/components/user/gender-input.tsx create mode 100644 app/frontend/src/javascript/components/user/password-input.tsx create mode 100644 app/frontend/src/stylesheets/modules/user/gender-input.scss create mode 100644 app/frontend/src/stylesheets/modules/user/user-profile-form.scss diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index a2a1a3405..af20152b6 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -3,11 +3,7 @@ # Devise controller to handle validation of email addresses class ConfirmationsController < Devise::ConfirmationsController # The path used after confirmation. - def after_confirmation_path_for(resource_name, resource) - if signed_in?(resource_name) - signed_in_root_path(resource) - else - signed_in_root_path(resource) - end + def after_confirmation_path_for(_resource_name, resource) + signed_in_root_path(resource) end end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index a397e4396..1bdd0e19e 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -13,4 +13,9 @@ class PasswordsController < Devise::PasswordsController head 404 end end + + # POST /password/verify + def verify + current_user.valid_password?(params[:password]) ? head(200) : head(404) + end end diff --git a/app/frontend/src/javascript/api/auth-provider.ts b/app/frontend/src/javascript/api/auth-provider.ts index 61ad4c5e7..90a2cd618 100644 --- a/app/frontend/src/javascript/api/auth-provider.ts +++ b/app/frontend/src/javascript/api/auth-provider.ts @@ -1,4 +1,4 @@ -import { AuthenticationProvider, MappingFields } from '../models/authentication-provider'; +import { ActiveProviderResponse, AuthenticationProvider, MappingFields } from '../models/authentication-provider'; import { AxiosResponse } from 'axios'; import apiClient from './clients/api-client'; @@ -36,4 +36,9 @@ export default class AuthProviderAPI { const res: AxiosResponse = await apiClient.get(`/api/auth_providers/strategy_name?providable_type=${authProvider.providable_type}&name=${authProvider.name}`); return res?.data; } + + static async active (): Promise { + const res: AxiosResponse = await apiClient.get('/api/auth_providers/active'); + return res?.data; + } } diff --git a/app/frontend/src/javascript/api/authentication.ts b/app/frontend/src/javascript/api/authentication.ts new file mode 100644 index 000000000..139b9abbf --- /dev/null +++ b/app/frontend/src/javascript/api/authentication.ts @@ -0,0 +1,23 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { User } from '../models/user'; + +export default class Authentication { + static async login (email: string, password: string): Promise { + const res: AxiosResponse = await apiClient.post('/users/sign_in.json', { email, password }); + return res?.data; + } + + static async logout (): Promise { + return apiClient.delete('/users/sign_out.json'); + } + + static async verifyPassword (password: string): Promise { + try { + const res: AxiosResponse = await apiClient.post('/password/verify.json', { password }); + return (res.status === 200); + } catch (e) { + return false; + } + } +} diff --git a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx index 675c07b51..fab041726 100644 --- a/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/openid-connect-form.tsx @@ -85,7 +85,7 @@ export const OpenidConnectForm = extends InputHTMLAttributes({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce }: FormInputProps) => { const [isDirty, setIsDirty] = useState(false); + const [fieldError, setFieldError] = useState(error); useEffect(() => { setIsDirty(_get(formState?.dirtyFields, id)); + setFieldError(_get(formState?.errors, id)); }, [formState]); + useEffect(() => { + setFieldError(error); + }, [error]); + /** * Debounced (ie. temporised) version of the 'on change' callback. */ @@ -48,8 +54,8 @@ export const FormInput = ({ id, register, labe 'form-input form-item', `${className || ''}`, `${type === 'hidden' ? 'is-hidden' : ''}`, - `${isDirty && error && error[id] ? 'is-incorrect' : ''}`, - `${isDirty && warning && warning[id] ? 'is-warned' : ''}`, + `${isDirty && fieldError ? 'is-incorrect' : ''}`, + `${isDirty && warning ? 'is-warned' : ''}`, `${rules && rules.required ? 'is-required' : ''}`, `${readOnly ? 'is-readonly' : ''}`, `${disabled ? 'is-disabled' : ''}` @@ -80,8 +86,8 @@ export const FormInput = ({ id, register, labe placeholder={placeholder} /> {addOn && {addOn}} - {(isDirty && error && error[id]) &&
{error[id].message}
} - {(isDirty && warning && warning[id]) &&
{warning[id].message}
} + {(isDirty && fieldError) &&
{fieldError.message}
} + {(isDirty && warning) &&
{warning.message}
} ); }; diff --git a/app/frontend/src/javascript/components/form/form-multi-select.tsx b/app/frontend/src/javascript/components/form/form-multi-select.tsx index ff03ee0e8..61ec8514e 100644 --- a/app/frontend/src/javascript/components/form/form-multi-select.tsx +++ b/app/frontend/src/javascript/components/form/form-multi-select.tsx @@ -32,7 +32,7 @@ export const FormMultiSelect = } /> - {(error && error[id]) &&
{error[id].message}
} + {(error) &&
{error.message}
} ); }; diff --git a/app/frontend/src/javascript/components/form/form-select.tsx b/app/frontend/src/javascript/components/form/form-select.tsx index ca13e40e9..f15e94f85 100644 --- a/app/frontend/src/javascript/components/form/form-select.tsx +++ b/app/frontend/src/javascript/components/form/form-select.tsx @@ -33,7 +33,7 @@ export const FormSelect = } /> - {(error && error[id]) &&
{error[id].message}
} + {(error) &&
{error.message}
} ); }; diff --git a/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx b/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx index b31a6d544..2f878d5f5 100644 --- a/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx +++ b/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx @@ -10,6 +10,7 @@ import { react2angular } from 'react2angular'; import { IApplication } from '../../models/application'; import LocalPaymentAPI from '../../api/local-payment'; import { PaymentMethod } from '../../models/payment'; +import { TDateISO } from '../../typings/date-iso'; declare const Application: IApplication; @@ -46,7 +47,7 @@ const FreeExtendModal: React.FC = ({ isOpen, toggleModal, /** * Return the formatted localized date for the given date */ - const formatDateTime = (date: Date): string => { + const formatDateTime = (date: TDateISO): string => { return t('app.admin.free_extend_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) }); }; diff --git a/app/frontend/src/javascript/components/user/change-password.tsx b/app/frontend/src/javascript/components/user/change-password.tsx new file mode 100644 index 000000000..73d41e83e --- /dev/null +++ b/app/frontend/src/javascript/components/user/change-password.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { FabButton } from '../base/fab-button'; +import { FabModal } from '../base/fab-modal'; +import { FormInput } from '../form/form-input'; +import { useForm, UseFormRegister } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import Authentication from '../../api/authentication'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { PasswordInput } from './password-input'; +import { FormState } from 'react-hook-form/dist/types/form'; + +interface ChangePasswordProp { + register: UseFormRegister, + onError: (message: string) => void, + currentFormPassword: string, + formState: FormState, +} + +/** + * This component shows a button that trigger a modal dialog to verify the user's current password. + * If the user's current password is correct, the modal dialog is closed and the button is replaced by a form to set the new password. + */ +export const ChangePassword = ({ register, onError, currentFormPassword, formState }: ChangePasswordProp) => { + const { t } = useTranslation('shared'); + + const [isModalOpen, setIsModalOpen] = React.useState(false); + const [isConfirmedPassword, setIsConfirmedPassword] = React.useState(false); + + const passwordConfirmationForm = useForm<{ password: string }>(); + + /** + * Opens/closes the dialog asking to confirm the current password before changing it. + */ + const toggleConfirmationModal = () => { + setIsModalOpen(!isModalOpen); + }; + + /** + * Callback triggered when the user confirms his current password. + */ + const onSubmit = (data: { password: string }) => { + Authentication.verifyPassword(data.password).then(res => { + if (res) { + setIsConfirmedPassword(true); + toggleConfirmationModal(); + } else { + onError(t('app.shared.change_password.wrong_password')); + } + }).catch(err => { + onError(err); + }); + }; + + return ( +
+ {!isConfirmedPassword && toggleConfirmationModal()}> + {t('app.shared.change_password.change_my_password')} + } + {isConfirmedPassword &&
+ +
} + +
+ + + {t('app.shared.change_password.confirm')} + + +
+
+ ); +}; diff --git a/app/frontend/src/javascript/components/user/gender-input.tsx b/app/frontend/src/javascript/components/user/gender-input.tsx new file mode 100644 index 000000000..e831e7a75 --- /dev/null +++ b/app/frontend/src/javascript/components/user/gender-input.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { UseFormRegister } from 'react-hook-form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { FieldPath } from 'react-hook-form/dist/types/path'; +import { useTranslation } from 'react-i18next'; + +interface GenderInputProps { + register: UseFormRegister, +} + +/** + * Input component to set the gender for the user + */ +export const GenderInput = ({ register }: GenderInputProps) => { + const { t } = useTranslation('shared'); + + return ( +
+ + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/user/password-input.tsx b/app/frontend/src/javascript/components/user/password-input.tsx new file mode 100644 index 000000000..546b30783 --- /dev/null +++ b/app/frontend/src/javascript/components/user/password-input.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { UseFormRegister } from 'react-hook-form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { useTranslation } from 'react-i18next'; +import { FormInput } from '../form/form-input'; +import { FormState } from 'react-hook-form/dist/types/form'; + +interface PasswordInputProps { + register: UseFormRegister, + currentFormPassword: string, + formState: FormState, +} + +/** + * Passwords inputs: new password and confirmation. + */ +export const PasswordInput = ({ register, currentFormPassword, formState }: PasswordInputProps) => { + const { t } = useTranslation('shared'); + + return ( +
+ { + if (value.length < 8) { + return t('app.shared.password_input.password_too_short') as string; + } + return true; + } + }} + formState={formState} + label={t('app.shared.password_input.new_password')} + type="password" /> + { + if (value !== currentFormPassword) { + return t('app.shared.password_input.confirmation_mismatch') as string; + } + return true; + } + }} + formState={formState} + label={t('app.shared.password_input.confirm_password')} + type="password" /> +
+ ); +}; diff --git a/app/frontend/src/javascript/components/user/user-profile-form.tsx b/app/frontend/src/javascript/components/user/user-profile-form.tsx index 7f2118ee0..398294cb9 100644 --- a/app/frontend/src/javascript/components/user/user-profile-form.tsx +++ b/app/frontend/src/javascript/components/user/user-profile-form.tsx @@ -1,21 +1,34 @@ import React from 'react'; import { react2angular } from 'react2angular'; -import { SubmitHandler, useForm } from 'react-hook-form'; +import { SubmitHandler, useForm, useWatch } from 'react-hook-form'; import { User } from '../../models/user'; import { IApplication } from '../../models/application'; import { Loader } from '../base/loader'; import { FormInput } from '../form/form-input'; +import { useTranslation } from 'react-i18next'; +import { Avatar } from './avatar'; +import { GenderInput } from './gender-input'; +import { ChangePassword } from './change-password'; +import Switch from 'react-switch'; +import { PasswordInput } from './password-input'; declare const Application: IApplication; interface UserProfileFormProps { action: 'create' | 'update', - user: User; - className?: string; + size?: 'small' | 'large', + user: User, + className?: string, + onError: (message: string) => void, } -export const UserProfileForm: React.FC = ({ action, user, className }) => { - const { handleSubmit, register } = useForm({ defaultValues: { ...user } }); +export const UserProfileForm: React.FC = ({ action, size, user, className, onError }) => { + const { t } = useTranslation('shared'); + + const { handleSubmit, register, control, formState } = useForm({ defaultValues: { ...user } }); + const output = useWatch({ control }); + + const [isOrganization, setIsOrganization] = React.useState(user.invoicing_profile.organization !== null); /** * Callback triggered when the form is submitted: process with the user creation or update. @@ -25,12 +38,106 @@ export const UserProfileForm: React.FC = ({ action, user, }; return ( -
- + +
+ +
+
+
+

{t('app.shared.user_profile_form.personal_data')}

+ +
+ + +
+
+ + +
+
+ + +
+
+
+

{t('app.shared.user_profile_form.account_data')}

+ + + {/* TODO: no password change if sso */} + {action === 'update' && } + {action === 'create' && } +
+
+

{t('app.shared.user_profile_form.organization_data')}

+ + {isOrganization &&
+ + + + +
} +
+
); }; +UserProfileForm.defaultProps = { + size: 'large' +}; + const UserProfileFormWrapper: React.FC = (props) => { return ( @@ -39,4 +146,4 @@ const UserProfileFormWrapper: React.FC = (props) => { ); }; -Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'user', 'className'])); +Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError'])); diff --git a/app/frontend/src/javascript/controllers/members.js b/app/frontend/src/javascript/controllers/members.js index 042146706..80e93609f 100644 --- a/app/frontend/src/javascript/controllers/members.js +++ b/app/frontend/src/javascript/controllers/members.js @@ -275,6 +275,14 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco $injector.get('$state').reload(); }; + /** + * Callback triggered when an error is raised on a lower-level component + * @param message {string} + */ + $scope.onError = function (message) { + growl.error(message); + }; + /* PRIVATE SCOPE */ /** diff --git a/app/frontend/src/javascript/lib/format.ts b/app/frontend/src/javascript/lib/format.ts index 811630602..16337d780 100644 --- a/app/frontend/src/javascript/lib/format.ts +++ b/app/frontend/src/javascript/lib/format.ts @@ -1,5 +1,6 @@ import moment, { unitOfTime } from 'moment'; import { IFablab } from '../models/fablab'; +import { TDateISO } from '../typings/date-iso'; declare let Fablab: IFablab; @@ -7,14 +8,14 @@ export default class FormatLib { /** * Return the formatted localized date for the given date */ - static date = (date: Date): string => { + static date = (date: Date|TDateISO): string => { return Intl.DateTimeFormat().format(moment(date).toDate()); }; /** * Return the formatted localized time for the given date */ - static time = (date: Date): string => { + static time = (date: Date|TDateISO): string => { return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate()); }; diff --git a/app/frontend/src/javascript/models/authentication-provider.ts b/app/frontend/src/javascript/models/authentication-provider.ts index 22f779174..0c2021317 100644 --- a/app/frontend/src/javascript/models/authentication-provider.ts +++ b/app/frontend/src/javascript/models/authentication-provider.ts @@ -65,3 +65,11 @@ export interface MappingFields { user: Array<[string, mappingType]>, profile: Array<[string, mappingType]> } + +export interface ActiveProviderResponse extends AuthenticationProvider { + previous_provider?: AuthenticationProvider + mapping: Array, + link_to_sso_profile: string, + link_to_sso_connect: string, + domain?: string +} diff --git a/app/frontend/src/javascript/models/form-component.ts b/app/frontend/src/javascript/models/form-component.ts index 5b0f8d58d..a611773eb 100644 --- a/app/frontend/src/javascript/models/form-component.ts +++ b/app/frontend/src/javascript/models/form-component.ts @@ -1,28 +1,33 @@ -import { FieldErrors, UseFormRegister, Validate } from 'react-hook-form'; +import { UseFormRegister, Validate } from 'react-hook-form'; import { Control, FormState } from 'react-hook-form/dist/types/form'; -export type ruleTypes = { +export type ruleTypes = { required?: boolean | string, pattern?: RegExp | { value: RegExp, message: string }, minLength?: number, maxLength?: number, min?: number, max?: number, - validate?: Validate; + validate?: Validate; }; +/** + * `error` and `warning` props can be manually set. + * Automatic error handling is done through the `formState` prop. + * Even for manual error/warning, the `formState` prop is required, because it is used to determine is the field is dirty. + */ export interface FormComponent { register: UseFormRegister, - error?: FieldErrors, - warning?: FieldErrors, - rules?: ruleTypes, + error?: { message: string }, + warning?: { message: string }, + rules?: ruleTypes, formState?: FormState; } export interface FormControlledComponent { control: Control, - error?: FieldErrors, - warning?: FieldErrors, - rules?: ruleTypes, + error?: { message: string }, + warning?: { message: string }, + rules?: ruleTypes, formState?: FormState; } diff --git a/app/frontend/src/javascript/models/payment.ts b/app/frontend/src/javascript/models/payment.ts index e8e9caba0..a3bcd5424 100644 --- a/app/frontend/src/javascript/models/payment.ts +++ b/app/frontend/src/javascript/models/payment.ts @@ -1,6 +1,5 @@ import { Reservation } from './reservation'; import { SubscriptionRequest } from './subscription'; -import { TDateISO } from '../typings/date-iso'; export interface PaymentConfirmation { requires_action?: boolean, @@ -27,7 +26,7 @@ export enum PaymentMethod { export type CartItem = { reservation: Reservation }| { subscription: SubscriptionRequest }| { prepaid_pack: { id: number } }| - { free_extension: { end_at: TDateISO } }; + { free_extension: { end_at: Date } }; export interface ShoppingCart { customer_id: number, diff --git a/app/frontend/src/javascript/models/user.ts b/app/frontend/src/javascript/models/user.ts index eb3ce02db..63d0d7c44 100644 --- a/app/frontend/src/javascript/models/user.ts +++ b/app/frontend/src/javascript/models/user.ts @@ -17,6 +17,8 @@ export interface User { need_completion: boolean, ip_address: string, mapped_from_sso?: string[], + password?: string, + password_confirmation?: string, profile: { id: number, first_name: string, diff --git a/app/frontend/src/javascript/typings/date-iso.d.ts b/app/frontend/src/javascript/typings/date-iso.d.ts index c86e84a06..fbe43d178 100644 --- a/app/frontend/src/javascript/typings/date-iso.d.ts +++ b/app/frontend/src/javascript/typings/date-iso.d.ts @@ -1,5 +1,13 @@ // from https://gist.github.com/MrChocolatine/367fb2a35d02f6175cc8ccb3d3a20054 +interface Date { + /** + * Give a more precise return type to the method `toISOString()`: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + */ + toISOString(): TDateISO; +} + type TYear = `${number}${number}${number}${number}`; type TMonth = `${number}${number}`; type TDay = `${number}${number}`; diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 777574b3d..4652052fb 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -73,6 +73,8 @@ @import "modules/subscriptions/free-extend-modal"; @import "modules/subscriptions/renew-modal"; @import "modules/user/avatar"; +@import "modules/user/gender-input"; +@import "modules/user/user-profile-form"; @import "modules/abuses"; @import "modules/cookies"; diff --git a/app/frontend/src/stylesheets/modules/user/gender-input.scss b/app/frontend/src/stylesheets/modules/user/gender-input.scss new file mode 100644 index 000000000..f639c5eee --- /dev/null +++ b/app/frontend/src/stylesheets/modules/user/gender-input.scss @@ -0,0 +1,36 @@ +.gender-input { + margin-bottom: 1.6rem; + + label { + display: inline-flex; + justify-content: flex-start; + flex-direction: row-reverse; + border: 1px solid #c9c9c9; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + font-weight: 400; + line-height: 1.5; + text-align: center; + touch-action: manipulation; + user-select: none; + vertical-align: middle; + white-space: nowrap; + position: relative; + max-width: 100%; + background-color: #fbfbfb; + color: #000; + margin-bottom: 0; + margin-top: 0; + padding: 7px 12px 6px; + margin-right: 16px; + + p { + margin: 0 8px; + } + + input { + margin: 5px 0 0; + } + } +} diff --git a/app/frontend/src/stylesheets/modules/user/user-profile-form.scss b/app/frontend/src/stylesheets/modules/user/user-profile-form.scss new file mode 100644 index 000000000..fdd54e3b7 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/user/user-profile-form.scss @@ -0,0 +1,43 @@ +.user-profile-form { + display: flex; + flex-direction: row; + + .fields-group { + width: 100%; + + & > * { + margin-top: 1.5rem; + } + + .names, .birth-phone { + display: flex; + flex-direction: row; + + .form-input:first-child { + margin-right: 32px; + } + } + + .organization-toggle { + p { + font-family: var(--font-text); + font-weight: normal; + font-size: 1.4rem; + line-height: normal; + margin: 0; + } + } + } + + &--small { + flex-direction: column; + + .names, .birth-phone { + flex-direction: column; + + .form-input:first-child { + margin-right: 0; + } + } + } +} diff --git a/app/frontend/templates/dashboard/settings.html b/app/frontend/templates/dashboard/settings.html index 4d8e804b6..c27453d4a 100644 --- a/app/frontend/templates/dashboard/settings.html +++ b/app/frontend/templates/dashboard/settings.html @@ -121,7 +121,7 @@
- +
diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 49cbf5abc..688bdc9db 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -33,30 +33,19 @@ en: add_video: "Embed a video" add_image: "Insert an image" #user edition form - user: - man: "Man" - woman: "Woman" + user_profile_form: add_an_avatar: "Add an avatar" - pseudonym: "Pseudonym" - pseudonym_is_required: "Pseudonym is required." - first_name: "Your first name" - first_name_is_required: "First name is required." - surname: "Your last name" - surname_is_required: "Last name is required." + personal_data: "Personal" + account_data: "Account" + organization_data: "Organization" + declare_organization: "I am an organization" + pseudonym: "Nickname" + first_name: "First name" + surname: "Surname" email_address: "Email address" - email_address_is_required: "E-mail address is required." - change_password: "Change password" - new_password: "New password" - password_is_required: "Password is required." - password_is_too_short: "Password is too short (at least 8 characters)" - confirmation_of_new_password: "Confirmation of new password" - confirmation_of_password_is_required: "Confirmation of password is required." - confirmation_of_password_is_too_short: "Confirmation of password is too short (minimum 8 characters)." - confirmation_mismatch_with_password: "Confirmation mismatch with password." organization_name: "Organization name" organization_address: "Organization address" date_of_birth: "Date of birth" - date_of_birth_is_required: "Date of birth is required." website: "Website" job: "Occupation" interests: "Interests" @@ -72,6 +61,19 @@ en: used_for_invoicing: "This data will be used for billing purposes" used_for_reservation: "This data will be used in case of change on one of your bookings" used_for_profile: "This data will only be displayed on your profile" + gender_input: + man: "Man" + woman: "Woman" + change_password: + change_my_password: "Change my password" + confirm_current: "Confirm your current password" + confirm: "OK" + wrong_password: "Wrong password" + password_input: + new_password: "New password" + confirm_password: "Confirm password" + password_too_short: "Password is too short (must be at least 8 characters)" + confirmation_mismatch: "Confirmation mismatch with password." #project edition form project: name: "Name" diff --git a/config/routes.rb b/config/routes.rb index caf8e5506..08bbb3aaf 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ Rails.application.routes.draw do devise_scope :user do get '/sessions/sign_out', to: 'devise/sessions#destroy' + post '/password/verify', to: 'passwords#verify' end ## The priority is based upon order of creation: first created -> highest priority. diff --git a/package.json b/package.json index 0ccc6b904..4e0323547 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "react": "^17.0.2", "react-cool-onclickoutside": "^1.7.0", "react-dom": "^17.0.2", - "react-hook-form": "^7.25.3", + "react-hook-form": "^7.30.0", "react-i18next": "^11.15.6", "react-modal": "^3.11.2", "react-select": "^5.2.2", diff --git a/yarn.lock b/yarn.lock index 4e69346f7..bbfb45920 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6035,10 +6035,10 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" -react-hook-form@^7.25.3: - version "7.25.3" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.25.3.tgz#1475fd52398e905e1f6d88835f96aaa1144635c3" - integrity sha512-jL4SByMaC8U3Vhu9s7CwgJBP4M6I3Kpwxib9LrCwWSRPnXDrNQL4uihSTqLLoDICqSUhwwvian9uVYfv+ITtGg== +react-hook-form@^7.30.0: + version "7.30.0" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.30.0.tgz#c9e2fd54d3627e43bd94bf38ef549df2e80c1371" + integrity sha512-DzjiM6o2vtDGNMB9I4yCqW8J21P314SboNG1O0obROkbg7KVS0I7bMtwSdKyapnCPjHgnxc3L7E5PEdISeEUcQ== react-i18next@^11.15.6: version "11.15.6"