diff --git a/src/frontend/apps/impress/src/features/addMembers/api/index.ts b/src/frontend/apps/impress/src/features/addMembers/api/index.ts deleted file mode 100644 index 1b0d23c0..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/api/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './useCreateInvitation'; -export * from './useCreateTeamAccess'; -export * from './useUsers'; diff --git a/src/frontend/apps/impress/src/features/addMembers/api/useCreateInvitation.tsx b/src/frontend/apps/impress/src/features/addMembers/api/useCreateInvitation.tsx deleted file mode 100644 index 6e3b8bc9..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/api/useCreateInvitation.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { User } from '@/core/auth'; -import { Invitation } from '@/features/members'; -import { Role, Team } from '@/features/teams'; - -import { OptionType } from '../types'; - -interface CreateInvitationParams { - email: User['email']; - role: Role; - teamId: Team['id']; -} - -export const createInvitation = async ({ - email, - role, - teamId, -}: CreateInvitationParams): Promise => { - const response = await fetchAPI(`teams/${teamId}/invitations/`, { - method: 'POST', - body: JSON.stringify({ - email, - role, - }), - }); - - if (!response.ok) { - throw new APIError( - `Failed to create the invitation for ${email}`, - await errorCauses(response, { - value: email, - type: OptionType.INVITATION, - }), - ); - } - - return response.json() as Promise; -}; - -export function useCreateInvitation() { - return useMutation({ - mutationFn: createInvitation, - }); -} diff --git a/src/frontend/apps/impress/src/features/addMembers/api/useCreateTeamAccess.tsx b/src/frontend/apps/impress/src/features/addMembers/api/useCreateTeamAccess.tsx deleted file mode 100644 index 08f8e91d..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/api/useCreateTeamAccess.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { User } from '@/core/auth'; -import { Access, KEY_LIST_TEAM_ACCESSES } from '@/features/members'; -import { KEY_LIST_TEAM, KEY_TEAM, Role, Team } from '@/features/teams'; - -import { OptionType } from '../types'; - -interface CreateTeamAccessParams { - name: User['name']; - role: Role; - teamId: Team['id']; - userId: User['id']; -} - -export const createTeamAccess = async ({ - userId, - name, - role, - teamId, -}: CreateTeamAccessParams): Promise => { - const response = await fetchAPI(`teams/${teamId}/accesses/`, { - method: 'POST', - body: JSON.stringify({ - user: userId, - role, - }), - }); - - if (!response.ok) { - throw new APIError( - `Failed to add ${name} in the team.`, - await errorCauses(response, { - value: name, - type: OptionType.NEW_MEMBER, - }), - ); - } - - return response.json() as Promise; -}; - -export function useCreateTeamAccess() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: createTeamAccess, - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_TEAM], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_TEAM_ACCESSES], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_TEAM], - }); - }, - }); -} diff --git a/src/frontend/apps/impress/src/features/addMembers/api/useUsers.tsx b/src/frontend/apps/impress/src/features/addMembers/api/useUsers.tsx deleted file mode 100644 index d5af57d5..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/api/useUsers.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; - -import { APIError, APIList, errorCauses, fetchAPI } from '@/api'; -import { User } from '@/core/auth'; -import { Team } from '@/features/teams'; - -export type UsersParams = { - query: string; - teamId: Team['id']; -}; - -type UsersResponse = APIList; - -export const getUsers = async ({ - query, - teamId, -}: UsersParams): Promise => { - const queriesParams = []; - queriesParams.push(query ? `q=${query}` : ''); - queriesParams.push(teamId ? `team_id=${teamId}` : ''); - const queryParams = queriesParams.filter(Boolean).join('&'); - - const response = await fetchAPI(`users/?${queryParams}`); - - if (!response.ok) { - throw new APIError('Failed to get the users', await errorCauses(response)); - } - - return response.json() as Promise; -}; - -export const KEY_LIST_USER = 'users'; - -export function useUsers( - param: UsersParams, - queryConfig?: UseQueryOptions, -) { - return useQuery({ - queryKey: [KEY_LIST_USER, param], - queryFn: () => getUsers(param), - ...queryConfig, - }); -} diff --git a/src/frontend/apps/impress/src/features/addMembers/assets/add-member.svg b/src/frontend/apps/impress/src/features/addMembers/assets/add-member.svg deleted file mode 100644 index 08de3aae..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/assets/add-member.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - diff --git a/src/frontend/apps/impress/src/features/addMembers/components/ModalAddMembers.tsx b/src/frontend/apps/impress/src/features/addMembers/components/ModalAddMembers.tsx deleted file mode 100644 index 01c16064..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/components/ModalAddMembers.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import { - Button, - Modal, - ModalSize, - VariantType, - useToastProvider, -} from '@openfun/cunningham-react'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { createGlobalStyle } from 'styled-components'; - -import { APIError } from '@/api'; -import { Box, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { ChooseRole } from '@/features/members'; -import { Role, Team } from '@/features/teams'; - -import { useCreateInvitation, useCreateTeamAccess } from '../api'; -import IconAddMember from '../assets/add-member.svg'; -import { - OptionInvitation, - OptionNewMember, - OptionSelect, - OptionType, - isOptionNewMember, -} from '../types'; - -import { OptionsSelect, SearchMembers } from './SearchMembers'; - -const GlobalStyle = createGlobalStyle` - .c__modal { - overflow: visible; - } -`; - -type APIErrorMember = APIError<{ - value: string; - type: OptionType; -}>; - -interface ModalAddMembersProps { - currentRole: Role; - onClose: () => void; - team: Team; -} - -export const ModalAddMembers = ({ - currentRole, - onClose, - team, -}: ModalAddMembersProps) => { - const { colorsTokens } = useCunninghamTheme(); - const { t } = useTranslation(); - const [selectedMembers, setSelectedMembers] = useState([]); - const [selectedRole, setSelectedRole] = useState(Role.MEMBER); - const { toast } = useToastProvider(); - const { mutateAsync: createInvitation } = useCreateInvitation(); - const { mutateAsync: createTeamAccess } = useCreateTeamAccess(); - - const switchActions = (selectedMembers: OptionsSelect) => - selectedMembers.map(async (selectedMember) => { - switch (selectedMember.type) { - case OptionType.INVITATION: - await createInvitation({ - email: selectedMember.value.email, - role: selectedRole, - teamId: team.id, - }); - break; - - case OptionType.NEW_MEMBER: - await createTeamAccess({ - name: selectedMember.value.name, - role: selectedRole, - teamId: team.id, - userId: selectedMember.value.id, - }); - break; - } - - return selectedMember; - }); - - const toastOptions = { - duration: 4000, - }; - - const onError = (dataError: APIErrorMember['data']) => { - const messageError = - dataError?.type === OptionType.INVITATION - ? t(`Failed to create the invitation for {{email}}`, { - email: dataError?.value, - }) - : t(`Failed to add {{name}} in the team`, { - name: dataError?.value, - }); - - toast(messageError, VariantType.ERROR, toastOptions); - }; - - const onSuccess = (option: OptionSelect) => { - const message = !isOptionNewMember(option) - ? t('Invitation sent to {{email}}', { - email: option.value.email, - }) - : t('Member {{name}} added to the team', { - name: option.value.name, - }); - - toast(message, VariantType.SUCCESS, toastOptions); - }; - - const handleValidate = async () => { - const settledPromises = await Promise.allSettled< - OptionInvitation | OptionNewMember - >(switchActions(selectedMembers)); - - onClose(); - settledPromises.forEach((settledPromise) => { - switch (settledPromise.status) { - case 'rejected': - onError((settledPromise.reason as APIErrorMember).data); - break; - - case 'fulfilled': - onSuccess(settledPromise.value); - break; - } - }); - }; - - return ( - - {t('Cancel')} - - } - onClose={onClose} - closeOnClickOutside - hideCloseButton - rightActions={ - - } - size={ModalSize.MEDIUM} - title={ - - - - {t('Add a member')} - - - } - > - - - - {selectedMembers.length > 0 && ( - - - {t('Choose a role')} - - - - )} - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/addMembers/components/SearchMembers.tsx b/src/frontend/apps/impress/src/features/addMembers/components/SearchMembers.tsx deleted file mode 100644 index 0b821e46..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/components/SearchMembers.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Options } from 'react-select'; -import AsyncSelect from 'react-select/async'; - -import { Team } from '@/features/teams'; -import { isValidEmail } from '@/utils'; - -import { KEY_LIST_USER, useUsers } from '../api/useUsers'; -import { OptionSelect, OptionType } from '../types'; - -export type OptionsSelect = Options; - -interface SearchMembersProps { - team: Team; - selectedMembers: OptionsSelect; - setSelectedMembers: (value: OptionsSelect) => void; -} - -export const SearchMembers = ({ - team, - selectedMembers, - setSelectedMembers, -}: SearchMembersProps) => { - const { t } = useTranslation(); - const [input, setInput] = useState(''); - const [userQuery, setUserQuery] = useState(''); - const resolveOptionsRef = useRef<((value: OptionsSelect) => void) | null>( - null, - ); - const { data } = useUsers( - { query: userQuery, teamId: team.id }, - { - enabled: !!userQuery, - queryKey: [KEY_LIST_USER, { query: userQuery }], - }, - ); - - const options = data?.results; - - useEffect(() => { - if (!resolveOptionsRef.current || !options) { - return; - } - - const optionsFiltered = options.filter( - (user) => - !selectedMembers?.find( - (selectedUser) => selectedUser.value.email === user.email, - ), - ); - - let users: OptionsSelect = optionsFiltered.map((user) => ({ - value: user, - label: user.name || user.email, - type: OptionType.NEW_MEMBER, - })); - - if (userQuery && isValidEmail(userQuery)) { - const isFoundUser = !!optionsFiltered.find( - (user) => user.email === userQuery, - ); - const isFoundEmail = !!selectedMembers.find( - (selectedMember) => selectedMember.value.email === userQuery, - ); - - if (!isFoundUser && !isFoundEmail) { - users = [ - { - value: { email: userQuery }, - label: userQuery, - type: OptionType.INVITATION, - }, - ]; - } - } - - resolveOptionsRef.current(users); - resolveOptionsRef.current = null; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [options, selectedMembers]); - - const loadOptions = (): Promise => { - return new Promise((resolve) => { - resolveOptionsRef.current = resolve; - }); - }; - - const timeout = useRef(null); - const onInputChangeHandle = useCallback((newValue: string) => { - setInput(newValue); - if (timeout.current) { - clearTimeout(timeout.current); - } - - timeout.current = setTimeout(() => { - setUserQuery(newValue); - }, 1000); - }, []); - - return ( - - t('Invite new members to {{teamName}}', { teamName: team.name }) - } - onChange={(value) => { - setInput(''); - setUserQuery(''); - setSelectedMembers(value); - }} - /> - ); -}; diff --git a/src/frontend/apps/impress/src/features/addMembers/index.ts b/src/frontend/apps/impress/src/features/addMembers/index.ts deleted file mode 100644 index 23fc0dd8..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components/ModalAddMembers'; diff --git a/src/frontend/apps/impress/src/features/addMembers/types.tsx b/src/frontend/apps/impress/src/features/addMembers/types.tsx deleted file mode 100644 index d33c83c2..00000000 --- a/src/frontend/apps/impress/src/features/addMembers/types.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { User } from '@/core/auth'; - -export enum OptionType { - INVITATION = 'invitation', - NEW_MEMBER = 'new_member', -} - -export const isOptionNewMember = ( - data: OptionSelect, -): data is OptionNewMember => { - return 'id' in data.value; -}; - -export interface OptionInvitation { - value: { email: string }; - label: string; - type: OptionType.INVITATION; -} - -export interface OptionNewMember { - value: User; - label: string; - type: OptionType.NEW_MEMBER; -} - -export type OptionSelect = OptionNewMember | OptionInvitation;