1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(wip)(ui) refactor user edition form

This commit is contained in:
Sylvain 2022-04-26 18:05:18 +02:00
parent 814ebfe52d
commit aa432d08b3
28 changed files with 477 additions and 63 deletions

View File

@ -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

View File

@ -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

View File

@ -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<string> = await apiClient.get(`/api/auth_providers/strategy_name?providable_type=${authProvider.providable_type}&name=${authProvider.name}`);
return res?.data;
}
static async active (): Promise<ActiveProviderResponse> {
const res: AxiosResponse<ActiveProviderResponse> = await apiClient.get('/api/auth_providers/active');
return res?.data;
}
}

View File

@ -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<User> {
const res: AxiosResponse<User> = await apiClient.post('/users/sign_in.json', { email, password });
return res?.data;
}
static async logout (): Promise<void> {
return apiClient.delete('/users/sign_out.json');
}
static async verifyPassword (password: string): Promise<boolean> {
try {
const res: AxiosResponse<never> = await apiClient.post('/password/verify.json', { password });
return (res.status === 200);
} catch (e) {
return false;
}
}
}

View File

@ -85,7 +85,7 @@ export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext ext
rules={{ required: true, pattern: urlRegex }}
onChange={checkForDiscoveryEndpoint}
debounce={400}
warning={!discoveryAvailable && { 'providable_attributes.issuer': { message: t('app.admin.authentication.openid_connect_form.discovery_unavailable') } }}
warning={!discoveryAvailable && { message: t('app.admin.authentication.openid_connect_form.discovery_unavailable') } }
formState={formState} />
<FormSelect id="providable_attributes.discovery"
label={t('app.admin.authentication.openid_connect_form.discovery')}

View File

@ -20,11 +20,17 @@ interface FormInputProps<TFieldValues> extends InputHTMLAttributes<HTMLInputElem
*/
export const FormInput = <TFieldValues extends FieldValues>({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, warning, formState, step, onChange, debounce }: FormInputProps<TFieldValues>) => {
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 = <TFieldValues extends FieldValues>({ 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 = <TFieldValues extends FieldValues>({ id, register, labe
placeholder={placeholder} />
{addOn && <span className={`addon ${addOnClassName || ''}`}>{addOn}</span>}
</div>
{(isDirty && error && error[id]) && <div className="form-item-error">{error[id].message}</div> }
{(isDirty && warning && warning[id]) && <div className="form-item-warning">{warning[id].message}</div> }
{(isDirty && fieldError) && <div className="form-item-error">{fieldError.message}</div> }
{(isDirty && warning) && <div className="form-item-warning">{warning.message}</div> }
</label>
);
};

View File

@ -32,7 +32,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
const classNames = [
'form-multi-select form-item',
`${className || ''}`,
`${error && error[id] ? 'is-incorrect' : ''}`,
`${error ? 'is-incorrect' : ''}`,
`${rules && rules.required ? 'is-required' : ''}`,
`${disabled ? 'is-disabled' : ''}`
].join(' ');
@ -75,7 +75,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
isMulti />
} />
</div>
{(error && error[id]) && <div className="form-item-error">{error[id].message}</div> }
{(error) && <div className="form-item-error">{error.message}</div> }
</label>
);
};

View File

@ -33,7 +33,7 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
const classNames = [
'form-select form-item',
`${className || ''}`,
`${error && error[id] ? 'is-incorrect' : ''}`,
`${error ? 'is-incorrect' : ''}`,
`${rules && rules.required ? 'is-required' : ''}`,
`${disabled ? 'is-disabled' : ''}`
].join(' ');
@ -76,7 +76,7 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
options={options} />
} />
</div>
{(error && error[id]) && <div className="form-item-error">{error[id].message}</div> }
{(error) && <div className="form-item-error">{error.message}</div> }
</label>
);
};

View File

@ -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<FreeExtendModalProps> = ({ 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) });
};

View File

@ -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<TFieldValues> {
register: UseFormRegister<TFieldValues>,
onError: (message: string) => void,
currentFormPassword: string,
formState: FormState<TFieldValues>,
}
/**
* 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 = <TFieldValues extends FieldValues>({ register, onError, currentFormPassword, formState }: ChangePasswordProp<TFieldValues>) => {
const { t } = useTranslation('shared');
const [isModalOpen, setIsModalOpen] = React.useState<boolean>(false);
const [isConfirmedPassword, setIsConfirmedPassword] = React.useState<boolean>(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 (
<div className="change-password">
{!isConfirmedPassword && <FabButton onClick={() => toggleConfirmationModal()}>
{t('app.shared.change_password.change_my_password')}
</FabButton>}
{isConfirmedPassword && <div className="password-fields">
<PasswordInput register={register} currentFormPassword={currentFormPassword} formState={formState} />
</div>}
<FabModal isOpen={isModalOpen} toggleModal={toggleConfirmationModal} title={t('app.shared.change_password.change_my_password')}>
<form onSubmit={passwordConfirmationForm.handleSubmit(onSubmit)}>
<FormInput id="password" type="password" register={passwordConfirmationForm.register} rules={{ required: true }} label={t('app.shared.change_password.confirm_current')} />
<FabButton type="submit">
{t('app.shared.change_password.confirm')}
</FabButton>
</form>
</FabModal>
</div>
);
};

View File

@ -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<TFieldValues> {
register: UseFormRegister<TFieldValues>,
}
/**
* Input component to set the gender for the user
*/
export const GenderInput = <TFieldValues extends FieldValues>({ register }: GenderInputProps<TFieldValues>) => {
const { t } = useTranslation('shared');
return (
<div className="gender-input">
<label>
<p>{t('app.shared.gender_input.man')}</p>
<input type="radio"
value="true"
{...register('statistic_profile_attributes.gender' as FieldPath<TFieldValues>)} />
</label>
<label>
<p>{t('app.shared.gender_input.woman')}</p>
<input type="radio"
value="false"
{...register('statistic_profile_attributes.gender' as FieldPath<TFieldValues>)} />
</label>
</div>
);
};

View File

@ -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<TFieldValues> {
register: UseFormRegister<TFieldValues>,
currentFormPassword: string,
formState: FormState<TFieldValues>,
}
/**
* Passwords inputs: new password and confirmation.
*/
export const PasswordInput = <TFieldValues extends FieldValues>({ register, currentFormPassword, formState }: PasswordInputProps<TFieldValues>) => {
const { t } = useTranslation('shared');
return (
<div className="password-input">
<FormInput id="password" register={register}
rules={{
required: true,
validate: (value: string) => {
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" />
<FormInput id="password_confirmation"
register={register}
rules={{
required: true,
validate: (value: string) => {
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" />
</div>
);
};

View File

@ -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<UserProfileFormProps> = ({ action, user, className }) => {
const { handleSubmit, register } = useForm<User>({ defaultValues: { ...user } });
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, className, onError }) => {
const { t } = useTranslation('shared');
const { handleSubmit, register, control, formState } = useForm<User>({ defaultValues: { ...user } });
const output = useWatch<User>({ control });
const [isOrganization, setIsOrganization] = React.useState<boolean>(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<UserProfileFormProps> = ({ action, user,
};
return (
<form className={`user-profile-form ${className}`} onSubmit={handleSubmit(onSubmit)}>
<FormInput id="email" register={register} rules={{ required: true }} label="email" />
<form className={`user-profile-form user-profile-form--${size} ${className}`} onSubmit={handleSubmit(onSubmit)}>
<div className="avatar-group">
<Avatar user={user}/>
</div>
<div className="fields-group">
<div className="personnal-data">
<h4>{t('app.shared.user_profile_form.personal_data')}</h4>
<GenderInput register={register} />
<div className="names">
<FormInput id="profile_attributes.last_name"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.shared.user_profile_form.surname')} />
<FormInput id="profile_attributes.first_name"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.shared.user_profile_form.first_name')} />
</div>
<div className="birth-phone">
<FormInput id="statistic_profile_attributes.birthday"
register={register}
label={t('app.shared.user_profile_form.date_of_birth')}
type="date" />
<FormInput id="profile_attributes.phone"
register={register}
rules={{
pattern: {
value: /^((00|\+)[0-9]{2,3})?[0-9]{4,14}$/,
message: t('app.shared.user_profile_form.phone_number_invalid')
}
}}
formState={formState}
label={t('app.shared.user_profile_form.phone_number')} />
</div>
<div className="address">
<FormInput id="invoicing_profile_attributes.address_attributes.id"
register={register}
type="hidden" />
<FormInput id="invoicing_profile_attributes.address_attributes.address"
register={register}
label={t('app.shared.user_profile_form.address')} />
</div>
</div>
<div className="account-data">
<h4>{t('app.shared.user_profile_form.account_data')}</h4>
<FormInput id="username"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.shared.user_profile_form.pseudonym')} />
<FormInput id="email"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.shared.user_profile_form.email_address')} />
{/* TODO: no password change if sso */}
{action === 'update' && <ChangePassword register={register}
onError={onError}
currentFormPassword={output.password}
formState={formState} />}
{action === 'create' && <PasswordInput register={register}
currentFormPassword={output.password}
formState={formState} />}
</div>
<div className="organization-data">
<h4>{t('app.shared.user_profile_form.organization_data')}</h4>
<label className="organization-toggle">
<p>{t('app.shared.user_profile_form.declare_organization')}</p>
<Switch checked={isOrganization} onChange={setIsOrganization} />
</label>
{isOrganization && <div className="organization-fields">
<FormInput id="invoicing_profile_attributes.organization_attributes.id"
register={register}
type="hidden" />
<FormInput id="invoicing_profile_attributes.organization_attributes.name"
register={register}
rules={{ required: isOrganization }}
formState={formState}
label={t('app.shared.user_profile_form.organization_name')} />
<FormInput id="invoicing_profile_attributes.organization_attributes.address_attributes.id"
register={register}
type="hidden" />
<FormInput id="invoicing_profile_attributes.organization_attributes.address_attributes.address"
register={register}
rules={{ required: isOrganization }}
formState={formState}
label={t('app.shared.user_profile_form.organization_address')} />
</div>}
</div>
</div>
</form>
);
};
UserProfileForm.defaultProps = {
size: 'large'
};
const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (props) => {
return (
<Loader>
@ -39,4 +146,4 @@ const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (props) => {
);
};
Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'user', 'className']));
Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError']));

View File

@ -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 */
/**

View File

@ -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());
};

View File

@ -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<string>,
link_to_sso_profile: string,
link_to_sso_connect: string,
domain?: string
}

View File

@ -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<TFieldValues> = {
export type ruleTypes = {
required?: boolean | string,
pattern?: RegExp | { value: RegExp, message: string },
minLength?: number,
maxLength?: number,
min?: number,
max?: number,
validate?: Validate<TFieldValues>;
validate?: Validate<unknown>;
};
/**
* `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<TFieldValues> {
register: UseFormRegister<TFieldValues>,
error?: FieldErrors,
warning?: FieldErrors,
rules?: ruleTypes<TFieldValues>,
error?: { message: string },
warning?: { message: string },
rules?: ruleTypes,
formState?: FormState<TFieldValues>;
}
export interface FormControlledComponent<TFieldValues, TContext extends object> {
control: Control<TFieldValues, TContext>,
error?: FieldErrors,
warning?: FieldErrors,
rules?: ruleTypes<TFieldValues>,
error?: { message: string },
warning?: { message: string },
rules?: ruleTypes,
formState?: FormState<TFieldValues>;
}

View File

@ -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,

View File

@ -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,

View File

@ -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}`;

View File

@ -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";

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -121,7 +121,7 @@
</section>
<section class="panel panel-default bg-light m">
<div class="panel-body m-r">
<user-profile-form user="user" action="'update'" />
<user-profile-form user="user" action="'update'" on-error="onError" />
<ng-include src="'/shared/_member_form.html'"></ng-include>
</div> <!-- ./panel-body -->
</section>

View File

@ -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"

View File

@ -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.

View File

@ -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",

View File

@ -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"