mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-03-15 12:29:16 +01:00
(ui) use user-profile-form in profile completion screen
This commit is contained in:
parent
55e76e1523
commit
69d595e9f6
@ -5,7 +5,7 @@ import { get as _get } from 'lodash';
|
||||
|
||||
export interface AbstractFormItemProps<TFieldValues> extends PropsWithChildren<AbstractFormComponent<TFieldValues>> {
|
||||
id: string,
|
||||
label?: string,
|
||||
label?: string|ReactNode,
|
||||
tooltip?: ReactNode,
|
||||
className?: string,
|
||||
disabled?: boolean|((id: string) => boolean),
|
||||
@ -24,6 +24,7 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
|
||||
|
||||
useEffect(() => {
|
||||
setIsDirty(_get(formState?.dirtyFields, id));
|
||||
console.log(_get(formState?.dirtyFields, id));
|
||||
setFieldError(_get(formState?.errors, id));
|
||||
}, [formState]);
|
||||
|
||||
|
@ -73,6 +73,7 @@ export const FormMultiSelect = <TFieldValues extends FieldValues, TContext exten
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={valuesDefault as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value, ref } }) =>
|
||||
<Select ref={ref}
|
||||
classNamePrefix="rs"
|
||||
|
@ -48,6 +48,7 @@ export const FormRichText = <TFieldValues extends FieldValues, TContext extends
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={valueDefault as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value } }) =>
|
||||
<FabTextEditor onChange={onChange}
|
||||
content={value}
|
||||
|
@ -53,6 +53,7 @@ export const FormSelect = <TFieldValues extends FieldValues, TContext extends ob
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={valueDefault as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value, ref } }) =>
|
||||
<Select ref={ref}
|
||||
classNamePrefix="rs"
|
||||
|
@ -33,6 +33,7 @@ export const FormSwitch = <TFieldValues, TContext extends object>({ id, label, t
|
||||
<Controller name={id as FieldPath<TFieldValues>}
|
||||
control={control}
|
||||
defaultValue={defaultValue as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
|
||||
rules={rules}
|
||||
render={({ field: { onChange, value, ref } }) =>
|
||||
<Switch onChange={val => {
|
||||
onChange(val);
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
import React, { ReactElement, useState } from 'react';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { User } from '../../models/user';
|
||||
import PaymentScheduleAPI from '../../api/payment-schedule';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import FormatLib from '../../lib/format';
|
||||
@ -58,7 +58,7 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
* Check if the current operator has administrative rights or is a normal member
|
||||
*/
|
||||
const isPrivileged = (): boolean => {
|
||||
return (operator.role === UserRole.Admin || operator.role === UserRole.Manager);
|
||||
return (operator.role === 'admin' || operator.role === 'manager');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { Plan } from '../../models/plan';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { User } from '../../models/user';
|
||||
import { Loader } from '../base/loader';
|
||||
import '../../lib/i18n';
|
||||
import FormatLib from '../../lib/format';
|
||||
@ -52,13 +52,13 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
|
||||
* Check if the user can subscribe to the current plan, for himself
|
||||
*/
|
||||
const canSubscribeForMe = (): boolean => {
|
||||
return operator?.role === UserRole.Member || (operator?.role === UserRole.Manager && userId === operator?.id);
|
||||
return operator?.role === 'member' || (operator?.role === 'manager' && userId === operator?.id);
|
||||
};
|
||||
/**
|
||||
* Check if the user can subscribe to the current plan, for someone else
|
||||
*/
|
||||
const canSubscribeForOther = (): boolean => {
|
||||
return operator?.role === UserRole.Admin || (operator?.role === UserRole.Manager && userId !== operator?.id);
|
||||
return operator?.role === 'admin' || (operator?.role === 'manager' && userId !== operator?.id);
|
||||
};
|
||||
/**
|
||||
* Check it the user has subscribed to this plan or not
|
||||
|
@ -67,7 +67,7 @@ export const AvatarInput = <TFieldValues extends FieldValues>({ currentAvatar, u
|
||||
|
||||
return (
|
||||
<div className={`avatar-input avatar-input--${size}`}>
|
||||
<Avatar avatar={avatar} userName={userName} size={size} />
|
||||
<Avatar avatar={avatar} userName={userName} size="large" />
|
||||
<div className="buttons">
|
||||
<FabButton onClick={onAddAvatar} className="select-button">
|
||||
{!hasAvatar() && <span>Add an avatar</span>}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||
import { useForm, useWatch, ValidateResult } from 'react-hook-form';
|
||||
import { isNil as _isNil } from 'lodash';
|
||||
import { User, UserFieldMapping } from '../../models/user';
|
||||
import { IApplication } from '../../models/application';
|
||||
@ -18,6 +18,12 @@ import { FabButton } from '../base/fab-button';
|
||||
import { EditSocials } from '../socials/edit-socials';
|
||||
import UserLib from '../../lib/user';
|
||||
import AuthProviderAPI from '../../api/auth-provider';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { Group } from '../../models/group';
|
||||
import GroupAPI from '../../api/group';
|
||||
import CustomAssetAPI from '../../api/custom-asset';
|
||||
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
|
||||
import { HtmlTranslate } from '../base/html-translate';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -28,9 +34,20 @@ interface UserProfileFormProps {
|
||||
className?: string,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (user: User) => void,
|
||||
showGroupInput?: boolean,
|
||||
showTermsAndConditionsInput?: boolean,
|
||||
}
|
||||
|
||||
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, className, onError, onSuccess }) => {
|
||||
/**
|
||||
* Option format, expected by react-select
|
||||
* @see https://github.com/JedWatson/react-select
|
||||
*/
|
||||
type selectOption = { value: number, label: string };
|
||||
|
||||
/**
|
||||
* Form component to create or update a user
|
||||
*/
|
||||
export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size, user, className, onError, onSuccess, showGroupInput = false, showTermsAndConditionsInput = false }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// regular expression to validate the input fields
|
||||
@ -42,37 +59,76 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
|
||||
const [isOrganization, setIsOrganization] = useState<boolean>(!_isNil(user.invoicing_profile_attributes.organization_attributes));
|
||||
const [isLocalDatabaseProvider, setIsLocalDatabaseProvider] = useState<boolean>(false);
|
||||
const [groups, setGroups] = useState<Group[]>([]);
|
||||
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
|
||||
|
||||
useEffect(() => {
|
||||
AuthProviderAPI.active().then(data => {
|
||||
setIsLocalDatabaseProvider(data.providable_type === 'DatabaseProvider');
|
||||
}).catch(error => onError(error));
|
||||
GroupAPI.index({ disabled: false, admins: user.role === 'admin' }).then(data => {
|
||||
setGroups(data);
|
||||
}).catch(error => onError(error));
|
||||
CustomAssetAPI.get(CustomAssetName.CguFile).then(data => {
|
||||
setTermsAndConditions(data);
|
||||
}).catch(error => onError(error));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Convert all groups to the react-select format
|
||||
*/
|
||||
const buildGroupsOptions = (): Array<selectOption> => {
|
||||
return groups.map(t => {
|
||||
return { value: t.id, label: t.name };
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the form is submitted: process with the user creation or update.
|
||||
*/
|
||||
const onSubmit: SubmitHandler<User> = (data: User) => {
|
||||
MemberAPI[action](data)
|
||||
.then(res => { onSuccess(res); })
|
||||
.catch((error) => { onError(error); });
|
||||
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
if (showTermsAndConditionsInput) {
|
||||
// When the form is submitted, we consider that the user should have accepted the terms and conditions,
|
||||
// so we mark the field as dirty, even if he doesn't touch it. Like that, the error message is displayed.
|
||||
setValue('cgu', !!output.cgu, { shouldDirty: true, shouldTouch: true });
|
||||
}
|
||||
|
||||
return handleSubmit((data: User) => {
|
||||
MemberAPI[action](data)
|
||||
.then(res => { onSuccess(res); })
|
||||
.catch((error) => { onError(error); });
|
||||
})(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given field path should be disabled because it's mapped to the SSO API.
|
||||
* Check if the given field path should be disabled
|
||||
*/
|
||||
const isDisabled = function (id: string) {
|
||||
// never allows admins to change their group
|
||||
if (id === 'group_id' && user.role === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the current provider is the local database, then all fields are enabled
|
||||
if (isLocalDatabaseProvider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the current provider is not the local database, then fields are disabled based on their mapping status.
|
||||
return user.mapped_from_sso?.includes(UserFieldMapping[id]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user has accepted the terms and conditions
|
||||
*/
|
||||
const checkAcceptTerms = function (value: boolean): ValidateResult {
|
||||
return value === true || (t('app.shared.user_profile_form.must_accept_terms') as string);
|
||||
};
|
||||
|
||||
const userNetworks = new UserLib(user).getUserSocialNetworks(user);
|
||||
|
||||
return (
|
||||
<form className={`user-profile-form user-profile-form--${size} ${className}`} onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className={`user-profile-form user-profile-form--${size} ${className}`} onSubmit={onSubmit}>
|
||||
<div className="avatar-group">
|
||||
<AvatarInput currentAvatar={output.profile_attributes.user_avatar_attributes?.attachment_url}
|
||||
userName={`${output.profile_attributes.first_name} ${output.profile_attributes.last_name}`}
|
||||
@ -231,6 +287,25 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
label={t('app.shared.user_profile_form.allow_newsletter')}
|
||||
tooltip={t('app.shared.user_profile_form.allow_newsletter_help')} />
|
||||
</div>
|
||||
{showGroupInput && <div className="group">
|
||||
<FormSelect options={buildGroupsOptions()}
|
||||
control={control}
|
||||
id="group_id"
|
||||
rules={{ required: true }}
|
||||
disabled={isDisabled}
|
||||
formState={formState}
|
||||
label={t('app.shared.user_profile_form.group')} />
|
||||
</div>}
|
||||
{showTermsAndConditionsInput && termsAndConditions && <div className="terms-and-conditions">
|
||||
<FormSwitch control={control}
|
||||
disabled={isDisabled}
|
||||
id="cgu"
|
||||
rules={{ validate: checkAcceptTerms }}
|
||||
formState={formState}
|
||||
label={<HtmlTranslate trKey="app.shared.user_profile_form.terms_and_conditions_html"
|
||||
options={{ POLICY_URL: termsAndConditions.custom_asset_file_attributes.attachment_url }} />}
|
||||
/>
|
||||
</div>}
|
||||
<div className="main-actions">
|
||||
<FabButton type="submit" className="submit-button">{t('app.shared.user_profile_form.save')}</FabButton>
|
||||
</div>
|
||||
@ -251,4 +326,4 @@ const UserProfileFormWrapper: React.FC<UserProfileFormProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError', 'onSuccess']));
|
||||
Application.Components.component('userProfileForm', react2angular(UserProfileFormWrapper, ['action', 'size', 'user', 'className', 'onError', 'onSuccess', 'showGroupInput', 'showTermsAndConditionsInput']));
|
||||
|
@ -36,7 +36,7 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
|
||||
$scope.groups = groupsPromise;
|
||||
|
||||
// current user, contains information retrieved from the SSO
|
||||
$scope.user = memberPromise;
|
||||
$scope.user = cleanUser(memberPromise);
|
||||
|
||||
// disallow the user to change his password as he connect from SSO
|
||||
$scope.preventPassword = true;
|
||||
@ -211,6 +211,26 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
|
||||
return !$scope.activeProvider.previous_provider || $scope.activeProvider.previous_provider.id === $scope.activeProvider.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when an error is raised on a lower-level component
|
||||
* @param message {string}
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user was successfully updated
|
||||
* @param user {object} the updated user
|
||||
*/
|
||||
$scope.onSuccess = function (user) {
|
||||
$scope.currentUser = _.cloneDeep(user);
|
||||
Auth._currentUser = _.cloneDeep(user);
|
||||
$rootScope.currentUser = _.cloneDeep(user);
|
||||
growl.success(_t('app.logged.profile_completion.your_profile_has_been_successfully_updated'));
|
||||
$state.go('app.public.home');
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -226,6 +246,13 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
|
||||
angular.forEach(activeProviderPromise.mapping, function (map) { $scope.preventField[map] = true; });
|
||||
};
|
||||
|
||||
// prepare the user for the react-hook-form
|
||||
function cleanUser (user) {
|
||||
delete user.$promise;
|
||||
delete user.$resolved;
|
||||
return user;
|
||||
}
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { User, UserRole } from '../models/user';
|
||||
import { supportedNetworks } from '../models/social-network';
|
||||
import { User } from '../models/user';
|
||||
import { supportedNetworks, SupportedSocialNetwork } from '../models/social-network';
|
||||
|
||||
export default class UserLib {
|
||||
private user: User;
|
||||
@ -12,9 +12,9 @@ export default class UserLib {
|
||||
* Check if the current user has privileged access for resources concerning the provided customer
|
||||
*/
|
||||
isPrivileged = (customer: User): boolean => {
|
||||
if (this.user.role === UserRole.Admin) return true;
|
||||
if (this.user.role === 'admin') return true;
|
||||
|
||||
if (this.user.role === UserRole.Manager) {
|
||||
if (this.user.role === 'manager') {
|
||||
return (this.user.id !== customer.id);
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export default class UserLib {
|
||||
const userNetworks = [];
|
||||
|
||||
for (const [name, url] of Object.entries(customer.profile_attributes)) {
|
||||
supportedNetworks.includes(name) && userNetworks.push({ name, url });
|
||||
supportedNetworks.includes(name as SupportedSocialNetwork) && userNetworks.push({ name, url });
|
||||
}
|
||||
return userNetworks;
|
||||
};
|
||||
|
@ -2,11 +2,7 @@ import { Plan } from './plan';
|
||||
import { TDateISO, TDateISODate } from '../typings/date-iso';
|
||||
import { supportedNetworks, SupportedSocialNetwork } from './social-network';
|
||||
|
||||
export enum UserRole {
|
||||
Member = 'member',
|
||||
Manager = 'manager',
|
||||
Admin = 'admin'
|
||||
}
|
||||
export type UserRole = 'member' | 'manager' | 'admin';
|
||||
|
||||
type ProfileAttributesSocial = {
|
||||
[network in SupportedSocialNetwork]: string
|
||||
@ -24,6 +20,7 @@ export interface User {
|
||||
mapped_from_sso?: string[],
|
||||
password?: string,
|
||||
password_confirmation?: string,
|
||||
cgu?: boolean, // Accepted terms and conditions?
|
||||
profile_attributes: ProfileAttributesSocial & {
|
||||
id: number,
|
||||
first_name: string,
|
||||
|
@ -31,4 +31,8 @@
|
||||
&--large {
|
||||
margin: 80px 40px;
|
||||
}
|
||||
|
||||
&--small {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
@ -63,51 +63,16 @@
|
||||
|
||||
<div class="panel-body m-r">
|
||||
<!-- common fields -->
|
||||
<ng-include src="'/shared/_member_form.html'"></ng-include>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3 col-sm-offset-1"></div>
|
||||
<div class="col-sm-offset-1 col-sm-6">
|
||||
<!-- group -->
|
||||
<div class="form-group" ng-class="{'has-error': userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$invalid}">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-users"></i>
|
||||
<span class="exponent m-l-xs help-cursor" title="{{ 'app.logged.profile_completion.used_for_statistics' | translate }}">
|
||||
<i class="fa fa-asterisk" aria-hidden="true"></i>
|
||||
</span>
|
||||
</span>
|
||||
<select ng-model="user.group_id" class="form-control" ng-disabled="user.role === 'admin'" required>
|
||||
<option value=null translate>{{ 'app.logged.profile_completion.your_user_s_profile' }}</option>
|
||||
<option ng-repeat="group in groups" ng-value="group.id" ng-selected="group.id == user.group_id">{{group.name}}</option>
|
||||
</select>
|
||||
<input type="hidden" name="user[group_id]" ng-value="user.group_id">
|
||||
</div>
|
||||
<span class="help-block" ng-show="userForm['user[group_id]'].$dirty && userForm['user[group_id]'].$error.required" translate>{{ 'app.logged.profile_completion.user_s_profile_is_required' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- accept cgu -->
|
||||
<div class="form-group" ng-class="{'has-error': userForm.cgu.$dirty && userForm.cgu.$invalid}" ng-show="cgu">
|
||||
<input type="checkbox"
|
||||
name="cgu"
|
||||
ng-model="user.cgu"
|
||||
value="true"
|
||||
ng-required="cgu != null"/> {{ 'app.logged.profile_completion.i_ve_read_and_i_accept_' | translate }}
|
||||
<a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ 'app.logged.profile_completion._the_fablab_policy' }}</a>
|
||||
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<user-profile-form user="user"
|
||||
size="'small'"
|
||||
action="'update'"
|
||||
on-error="onError"
|
||||
on-success="onSuccess"
|
||||
show-group-input="true"
|
||||
show-terms-and-conditions-input="true" />
|
||||
|
||||
</div> <!-- ./panel-body -->
|
||||
|
||||
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit"
|
||||
value="{{ 'app.shared.buttons.confirm_changes' | translate }}"
|
||||
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
|
||||
ng-disabled="userForm.$invalid"/>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -39,6 +39,7 @@ en:
|
||||
_click_on_the_synchronization_button_opposite_: "click on the synchronization button opposite"
|
||||
_disconnect_then_reconnect_: "disconnect then reconnect"
|
||||
_for_your_changes_to_take_effect: "for your changes to take effect."
|
||||
your_profile_has_been_successfully_updated: "Your profile has been successfully updated."
|
||||
dashboard:
|
||||
#dashboard: public profile
|
||||
profile:
|
||||
|
@ -72,6 +72,9 @@ 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"
|
||||
group: "Group"
|
||||
terms_and_conditions_html: "I've read and accept <a href=\"{POLICY_URL}\" target=\"_blank\">the terms and conditions<a/>"
|
||||
must_accept_terms: "You must accept the terms and conditions"
|
||||
save: "Save"
|
||||
gender_input:
|
||||
man: "Man"
|
||||
|
Loading…
x
Reference in New Issue
Block a user