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:
parent
814ebfe52d
commit
aa432d08b3
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
23
app/frontend/src/javascript/api/authentication.ts
Normal file
23
app/frontend/src/javascript/api/authentication.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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')}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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) });
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
33
app/frontend/src/javascript/components/user/gender-input.tsx
Normal file
33
app/frontend/src/javascript/components/user/gender-input.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
||||
|
@ -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 */
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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}`;
|
||||
|
@ -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";
|
||||
|
36
app/frontend/src/stylesheets/modules/user/gender-input.scss
Normal file
36
app/frontend/src/stylesheets/modules/user/gender-input.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user