From 1d2b814d6f51a59d415c7d9373ba74da9fc632d4 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 11 May 2022 14:37:39 +0200 Subject: [PATCH] (ui) react component to change user's group --- app/frontend/src/javascript/api/member.ts | 4 +- .../components/group/change-group.tsx | 117 ++++++++++++++++++ .../src/javascript/controllers/plans.js | 25 +++- app/frontend/src/stylesheets/application.scss | 1 + .../modules/group/change-group.scss | 50 ++++++++ app/frontend/templates/plans/index.html | 30 +---- config/locales/app.shared.en.yml | 6 + 7 files changed, 204 insertions(+), 29 deletions(-) create mode 100644 app/frontend/src/javascript/components/group/change-group.tsx create mode 100644 app/frontend/src/stylesheets/modules/group/change-group.scss diff --git a/app/frontend/src/javascript/api/member.ts b/app/frontend/src/javascript/api/member.ts index 457f9700d..d01c4be93 100644 --- a/app/frontend/src/javascript/api/member.ts +++ b/app/frontend/src/javascript/api/member.ts @@ -11,7 +11,7 @@ export default class MemberAPI { static async create (user: User): Promise { const data = serialize({ user }); - if (user.profile_attributes.user_avatar_attributes.attachment_files[0]) { + if (user.profile_attributes?.user_avatar_attributes?.attachment_files[0]) { data.set('user[profile_attributes][user_avatar_attributes][attachment]', user.profile_attributes.user_avatar_attributes.attachment_files[0]); } const res: AxiosResponse = await apiClient.post('/api/members', data, { @@ -24,7 +24,7 @@ export default class MemberAPI { static async update (user: User): Promise { const data = serialize({ user }); - if (user.profile_attributes.user_avatar_attributes.attachment_files[0]) { + if (user.profile_attributes?.user_avatar_attributes?.attachment_files[0]) { data.set('user[profile_attributes][user_avatar_attributes][attachment]', user.profile_attributes.user_avatar_attributes.attachment_files[0]); } const res: AxiosResponse = await apiClient.patch(`/api/members/${user.id}`, data, { diff --git a/app/frontend/src/javascript/components/group/change-group.tsx b/app/frontend/src/javascript/components/group/change-group.tsx new file mode 100644 index 000000000..2747bd576 --- /dev/null +++ b/app/frontend/src/javascript/components/group/change-group.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import { User } from '../../models/user'; +import { Loader } from '../base/loader'; +import { IApplication } from '../../models/application'; +import { react2angular } from 'react2angular'; +import { Group } from '../../models/group'; +import GroupAPI from '../../api/group'; +import { FabButton } from '../base/fab-button'; +import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { FormSelect } from '../form/form-select'; +import MemberAPI from '../../api/member'; + +declare const Application: IApplication; + +interface ChangeGroupProps { + user: User, + onSuccess: (message: string, user: User) => void, + onError: (message: string) => void, + allowChange?: boolean, +} + +/** + * Option format, expected by react-select + * @see https://github.com/JedWatson/react-select + */ +type selectOption = { value: number, label: string }; + +export const ChangeGroup: React.FC = ({ user, onSuccess, onError, allowChange }) => { + const { t } = useTranslation('shared'); + + const [groups, setGroups] = useState>([]); + const [changeRequested, setChangeRequested] = useState(false); + const [operator, setOperator] = useState(null); + + const { handleSubmit, control } = useForm(); + + useEffect(() => { + GroupAPI.index({ disabled: false, admins: false }).then(setGroups).catch(onError); + MemberAPI.current().then(setOperator).catch(onError); + }, []); + + useEffect(() => { + setChangeRequested(false); + }, [user, allowChange]); + + /** + * Displays or hide the form to change the group. + */ + const toggleChangeRequest = () => { + setChangeRequested(!changeRequested); + }; + + /** + * Check if the group changing is currently allowed. + */ + const canChangeGroup = (): boolean => { + return allowChange; + }; + + /** + * Convert the provided array of items to the react-select format + */ + const buildGroupsOptions = (): Array => { + return groups?.map(t => { + return { value: t.id, label: t.name }; + }); + }; + + /** + * Callback triggered when the group changing form is submitted. + */ + const onSubmit = (data: { group_id: number }) => { + MemberAPI.update({ id: user.id, group_id: data.group_id } as User).then(res => { + toggleChangeRequest(); + onSuccess(t('app.shared.change_group.success'), res); + }).catch(onError); + }; + + // do not render the component if no user were provided (we cannot change th group of nobody) + if (!user) return null; + + return ( +
+

{t('app.shared.change_group.title', { OPERATOR: operator?.id === user.id ? 'self' : 'admin' })}

+ {!changeRequested &&
+
+ {groups.find(group => group.id === user.group_id)?.name} +
+ {canChangeGroup() && + {t('app.shared.change_group.change', { OPERATOR: operator?.id === user.id ? 'self' : 'admin' })} + } +
} + {changeRequested &&
+ +
+ {t('app.shared.change_group.cancel')} + {t('app.shared.change_group.validate')} +
+ } +
+ ); +}; + +ChangeGroup.defaultProps = { + allowChange: true +}; + +const ChangeGroupWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('changeGroup', react2angular(ChangeGroupWrapper, ['user', 'onSuccess', 'onError', 'allowChange'])); diff --git a/app/frontend/src/javascript/controllers/plans.js b/app/frontend/src/javascript/controllers/plans.js index 41ec70dfe..b5cd15f57 100644 --- a/app/frontend/src/javascript/controllers/plans.js +++ b/app/frontend/src/javascript/controllers/plans.js @@ -76,8 +76,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop if ($scope.selectedPlan !== plan) { $scope.selectedPlan = plan; $scope.planSelectionTime = new Date(); - } else { - $scope.selectedPlan = null; } } else { $scope.login(); @@ -184,6 +182,29 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop $scope.coupon.applied = null; }; + /** + * Callback triggered when the user has successfully changed his group + */ + $scope.onGroupUpdateSuccess = function (message, user) { + growl.success(message); + setTimeout(() => { + $scope.ctrl.member = _.cloneDeep(user); + $scope.$apply(); + }, 50); + if (AuthService.isAuthorized('member') || + (AuthService.isAuthorized('manager') && $scope.currentUser.id !== $scope.ctrl.member.id)) { + $rootScope.currentUser.group_id = user.group_id; + Auth._currentUser.group_id = user.group_id; + } + }; + + /** + * Check if it is allowed the change the group of teh selected user + */ + $scope.isAllowedChangingGroup = function () { + return $scope.ctrl.member && !$scope.selectedPlan && !$scope.paid.plan; + }; + /* PRIVATE SCOPE */ /** diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index f4eb49da3..e88dbe6b5 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -33,6 +33,7 @@ @import "modules/form/form-item"; @import "modules/form/form-rich-text"; @import "modules/form/form-switch"; +@import "modules/group/change-group"; @import "modules/machines/machine-card"; @import "modules/machines/machines-filters"; @import "modules/machines/machines-list"; diff --git a/app/frontend/src/stylesheets/modules/group/change-group.scss b/app/frontend/src/stylesheets/modules/group/change-group.scss new file mode 100644 index 000000000..044e18647 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/group/change-group.scss @@ -0,0 +1,50 @@ +.change-group { + border: 1px solid #dddddd; + border-radius: var(--border-radius); + margin: 30px 15px; + + h3 { + margin: 0; + padding: 18px 15px; + line-height: 1.8rem; + font-size: 1.4rem; + font-weight: 600; + border-bottom: 1px solid #dddddd; + } + + .display, + .change-group-form { + padding: 15px; + } + + .display { + .current-group { + background-color: #999; + color: #000; + min-height: 20px; + border: 1px solid #999; + margin-bottom: 20px; + padding: 5px 10px; + border-radius: var(--border-radius); + font-size: 1.4rem; + font-weight: bold; + } + + .request-change-btn { + width: 100%; + } + } + + .change-group-form { + .actions { + display: flex; + justify-content: space-between; + + .validate-btn { + border-color: var(--information-light); + background-color: var(--information); + color: white; + } + } + } +} diff --git a/app/frontend/templates/plans/index.html b/app/frontend/templates/plans/index.html index 875f9f3c1..01f1479d5 100644 --- a/app/frontend/templates/plans/index.html +++ b/app/frontend/templates/plans/index.html @@ -39,31 +39,11 @@ -
-
-

{{ 'app.public.plans.my_group' }}

-

{{ 'app.public.plans.his_group' }}

-
-
-
-
- {{getUserGroup().name}} -
- -
-
- - -
-
-
+ +
diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index b1c7a5ddf..b12cfba7d 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -23,6 +23,12 @@ en: you_will_lose_any_unsaved_modification_if_you_reload_this_page: "You will lose any unsaved modification if you reload this page" payment_card_error: "A problem has occurred with your credit card:" payment_card_declined: "Your card was declined." + change_group: + title: "{OPERATOR, select, self{My group} other{User's group}}" + change: "Change {OPERATOR, select, self{my} other{his}} group" + cancel: "Cancel" + validate: "Validate group change" + success: "Group successfully changed" #text editor text_editor: text_placeholder: "Type something…"