🔥(app-impress) remove addMembers feature
addMembers feature was a part of the People project, we don't need it in the Impress project.
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
export * from './useCreateInvitation';
|
||||
export * from './useCreateTeamAccess';
|
||||
export * from './useUsers';
|
||||
@@ -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<Invitation> => {
|
||||
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<Invitation>;
|
||||
};
|
||||
|
||||
export function useCreateInvitation() {
|
||||
return useMutation<Invitation, APIError, CreateInvitationParams>({
|
||||
mutationFn: createInvitation,
|
||||
});
|
||||
}
|
||||
@@ -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<Access> => {
|
||||
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<Access>;
|
||||
};
|
||||
|
||||
export function useCreateTeamAccess() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Access, APIError, CreateTeamAccessParams>({
|
||||
mutationFn: createTeamAccess,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_TEAM],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_LIST_TEAM_ACCESSES],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: [KEY_TEAM],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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<User>;
|
||||
|
||||
export const getUsers = async ({
|
||||
query,
|
||||
teamId,
|
||||
}: UsersParams): Promise<UsersResponse> => {
|
||||
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<UsersResponse>;
|
||||
};
|
||||
|
||||
export const KEY_LIST_USER = 'users';
|
||||
|
||||
export function useUsers(
|
||||
param: UsersParams,
|
||||
queryConfig?: UseQueryOptions<UsersResponse, APIError, UsersResponse>,
|
||||
) {
|
||||
return useQuery<UsersResponse, APIError, UsersResponse>({
|
||||
queryKey: [KEY_LIST_USER, param],
|
||||
queryFn: () => getUsers(param),
|
||||
...queryConfig,
|
||||
});
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.34 26.04C20.9 26.02 20.46 26 20 26C15.16 26 10.64 27.34 6.78 29.64C5.02 30.68 4 32.64 4 34.7V40H22.52C20.94 37.74 20 34.98 20 32C20 29.86 20.5 27.86 21.34 26.04Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M20 24C24.4183 24 28 20.4183 28 16C28 11.5817 24.4183 8 20 8C15.5817 8 12 11.5817 12 16C12 20.4183 15.5817 24 20 24Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M33 24C28.032 24 24 28.032 24 33C24 37.968 28.032 42 33 42C37.968 42 42 37.968 42 33C42 28.032 37.968 24 33 24ZM36.6 33.9H33.9V36.6C33.9 37.095 33.495 37.5 33 37.5C32.505 37.5 32.1 37.095 32.1 36.6V33.9H29.4C28.905 33.9 28.5 33.495 28.5 33C28.5 32.505 28.905 32.1 29.4 32.1H32.1V29.4C32.1 28.905 32.505 28.5 33 28.5C33.495 28.5 33.9 28.905 33.9 29.4V32.1H36.6C37.095 32.1 37.5 32.505 37.5 33C37.5 33.495 37.095 33.9 36.6 33.9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 925 B |
@@ -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<OptionsSelect>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role>(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 (
|
||||
<Modal
|
||||
isOpen
|
||||
leftActions={
|
||||
<Button color="secondary" fullWidth onClick={onClose}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
}
|
||||
onClose={onClose}
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
rightActions={
|
||||
<Button
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={!selectedMembers.length}
|
||||
onClick={() => void handleValidate()}
|
||||
>
|
||||
{t('Validate')}
|
||||
</Button>
|
||||
}
|
||||
size={ModalSize.MEDIUM}
|
||||
title={
|
||||
<Box $align="center" $gap="1rem">
|
||||
<IconAddMember width={48} color={colorsTokens()['primary-text']} />
|
||||
<Text $size="h3" className="m-0">
|
||||
{t('Add a member')}
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<GlobalStyle />
|
||||
<Box className="mb-xl mt-l">
|
||||
<SearchMembers
|
||||
team={team}
|
||||
setSelectedMembers={setSelectedMembers}
|
||||
selectedMembers={selectedMembers}
|
||||
/>
|
||||
{selectedMembers.length > 0 && (
|
||||
<Box className="mt-s">
|
||||
<Text as="h4" $textAlign="left" className="mb-t">
|
||||
{t('Choose a role')}
|
||||
</Text>
|
||||
<ChooseRole
|
||||
currentRole={currentRole}
|
||||
disabled={false}
|
||||
defaultRole={Role.MEMBER}
|
||||
setRole={setSelectedRole}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -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<OptionSelect>;
|
||||
|
||||
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<OptionsSelect> => {
|
||||
return new Promise<OptionsSelect>((resolve) => {
|
||||
resolveOptionsRef.current = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const onInputChangeHandle = useCallback((newValue: string) => {
|
||||
setInput(newValue);
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
setUserQuery(newValue);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
aria-label={t('Find a member to add to the team')}
|
||||
isMulti
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions={[]}
|
||||
onInputChange={onInputChangeHandle}
|
||||
inputValue={input}
|
||||
placeholder={t('Search new members (name or email)')}
|
||||
noOptionsMessage={() =>
|
||||
t('Invite new members to {{teamName}}', { teamName: team.name })
|
||||
}
|
||||
onChange={(value) => {
|
||||
setInput('');
|
||||
setUserQuery('');
|
||||
setSelectedMembers(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './components/ModalAddMembers';
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user