diff --git a/frontend/src/components/Teams/TeamsList/TeamItem/TeamItem.spec.tsx b/frontend/src/components/Teams/TeamsList/TeamItem/TeamItem.spec.tsx index 29bd3f4b1..928247b82 100644 --- a/frontend/src/components/Teams/TeamsList/TeamItem/TeamItem.spec.tsx +++ b/frontend/src/components/Teams/TeamsList/TeamItem/TeamItem.spec.tsx @@ -8,7 +8,6 @@ import { TeamUserRoles } from '@/utils/enums/team.user.roles'; import TeamItem, { TeamItemProps } from './index'; const DEFAULT_PROPS = { - userId: '', team: TeamFactory.create(), }; @@ -18,8 +17,13 @@ jest.mock('next/router', () => ({ useRouter: () => router, })); -const render = (props: TeamItemProps = DEFAULT_PROPS) => - renderWithProviders(, { routerOptions: router }); +const render = (props: TeamItemProps = DEFAULT_PROPS, options?: any) => + renderWithProviders(, { + routerOptions: router, + sessionOptions: { + user: options?.user, + }, + }); describe('Components/TeamItem', () => { let testProps: TeamItemProps; @@ -32,10 +36,9 @@ describe('Components/TeamItem', () => { const teamItemProps = { ...testProps }; // Act - const { getByTestId, getByText } = render(teamItemProps); + const { getByText } = render(teamItemProps); // Assert - expect(getByTestId('teamitemTitle')).toBeInTheDocument(); expect(getByText(teamItemProps.team.name)).toBeInTheDocument(); }); @@ -50,11 +53,10 @@ describe('Components/TeamItem', () => { }; // Act - const { getByTestId } = render(teamItemProps); + const { getByText } = render(teamItemProps); // Assert - expect(getByTestId('teamitemBoards')).toBeInTheDocument(); - expect(getByTestId('teamitemBoards')).toHaveTextContent('3 team boards'); + expect(getByText('3 team boards')).toBeInTheDocument(); }); it('should render no team boards', () => { @@ -68,18 +70,16 @@ describe('Components/TeamItem', () => { }; // Act - const { getByTestId } = render(teamItemProps); + const { getByText } = render(teamItemProps); // Assert - expect(getByTestId('teamitemBoards')).toBeInTheDocument(); - expect(getByTestId('teamitemBoards')).toHaveTextContent('No boards'); + expect(getByText('No boards')).toBeInTheDocument(); }); it('should render create first board', () => { // Arrange const teamAdmin = TeamUserFactory.create({ role: TeamUserRoles.ADMIN }); const teamItemProps = { - userId: teamAdmin.user._id, team: { ...testProps.team, boardsCount: 0, @@ -88,10 +88,9 @@ describe('Components/TeamItem', () => { }; // Act - const { getByTestId } = render(teamItemProps); + const { getByText } = render(teamItemProps, { user: teamAdmin.user }); // Assert - expect(getByTestId('teamitemBoards')).toBeInTheDocument(); - expect(getByTestId('teamitemBoards')).toHaveTextContent('Create first board'); + expect(getByText('Create first board')).toBeInTheDocument(); }); }); diff --git a/frontend/src/components/Teams/TeamsList/TeamItem/index.tsx b/frontend/src/components/Teams/TeamsList/TeamItem/index.tsx index ffba62920..daa91ce78 100644 --- a/frontend/src/components/Teams/TeamsList/TeamItem/index.tsx +++ b/frontend/src/components/Teams/TeamsList/TeamItem/index.tsx @@ -30,18 +30,15 @@ const InnerContainer = styled(Flex, Box, { }); export type TeamItemProps = { - userId: string | undefined; team: Team; isTeamPage?: boolean; }; -const TeamItem = React.memo(({ userId, team, isTeamPage }) => { +const TeamItem = React.memo(({ team, isTeamPage }) => { const { data: session } = useSession(); - const router = useRouter(); - const isSAdmin = session?.user.isSAdmin; - + const { id: userId, isSAdmin } = { ...session?.user }; const { id, users, name } = team; const userFound: TeamUser | undefined = users.find((member) => member.user?._id === userId); const userRole = userFound?.role; @@ -61,7 +58,7 @@ const TeamItem = React.memo(({ userId, team, isTeamPage }) => { }, [isSAdmin, team, userId]); return ( - + { const renderTitle = () => ( - + {title} ); diff --git a/frontend/src/components/Teams/TeamsList/TeamsList.spec.tsx b/frontend/src/components/Teams/TeamsList/TeamsList.spec.tsx new file mode 100644 index 000000000..4c367e01c --- /dev/null +++ b/frontend/src/components/Teams/TeamsList/TeamsList.spec.tsx @@ -0,0 +1,52 @@ +import { createMockRouter } from '@/utils/testing/mocks'; +import { renderWithProviders } from '@/utils/testing/renderWithProviders'; +import { TeamFactory } from '@/utils/factories/team'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { ROUTES } from '@/utils/routes'; +import TeamsList, { TeamsListProps } from '.'; + +const DEFAULT_PROPS = { + teams: TeamFactory.createMany(3), +}; + +const router = createMockRouter({ pathname: '/teams' }); + +jest.mock('next/router', () => ({ + useRouter: () => router, +})); + +const render = (props: TeamsListProps = DEFAULT_PROPS) => + renderWithProviders(, { routerOptions: router }); + +describe('Components/TeamsList', () => { + let testProps: TeamsListProps; + beforeEach(() => { + testProps = { ...DEFAULT_PROPS }; + }); + + it('should render correctly', () => { + // Arrange + const teamItemProps = { ...testProps }; + + // Act + const { getAllByTestId } = render(teamItemProps); + + // Assert + expect(getAllByTestId('teamItem')).toHaveLength(teamItemProps.teams.length); + }); + + it('should render empty state correctly', async () => { + // Arrange + const teamItemProps = { ...testProps, teams: [] }; + + // Act + const { getByTestId, getByText } = render(teamItemProps); + fireEvent.click(getByText('Create your first team')); + + // Assert + expect(getByTestId('emptyTeams')).toBeInTheDocument(); + await waitFor(() => { + expect(router.push).toHaveBeenCalledWith(ROUTES.NewTeam, ROUTES.NewTeam, expect.anything()); + }); + }); +}); diff --git a/frontend/src/components/Teams/TeamsList/index.tsx b/frontend/src/components/Teams/TeamsList/index.tsx index 7f6c3d100..0cf7c2eef 100644 --- a/frontend/src/components/Teams/TeamsList/index.tsx +++ b/frontend/src/components/Teams/TeamsList/index.tsx @@ -1,19 +1,25 @@ import React from 'react'; import { Team } from '@/types/team/team'; +import Flex from '@/components/Primitives/Flex'; import EmptyTeams from './partials/EmptyTeams'; -import ListOfCards from './partials/ListOfCards'; -type TeamsListProps = { - userId: string; +import TeamItem from './TeamItem'; + +export type TeamsListProps = { teams: Team[]; - isFetching: boolean; }; -const TeamsList = ({ userId, teams, isFetching }: TeamsListProps) => { +const TeamsList = ({ teams }: TeamsListProps) => { if (teams?.length === 0) return ; - return ; + return ( + + {teams.map((team: Team) => ( + + ))} + + ); }; export default TeamsList; diff --git a/frontend/src/components/Teams/TeamsList/partials/EmptyTeams.tsx b/frontend/src/components/Teams/TeamsList/partials/EmptyTeams.tsx new file mode 100644 index 000000000..811fd0eea --- /dev/null +++ b/frontend/src/components/Teams/TeamsList/partials/EmptyTeams.tsx @@ -0,0 +1,37 @@ +import { styled } from '@/styles/stitches/stitches.config'; +import Link from 'next/link'; + +import Text from '@/components/Primitives/Text'; +import Box from '@/components/Primitives/Box'; +import Flex from '@/components/Primitives/Flex'; + +import EmptyTeamsImage from '@/components/images/EmptyTeams'; + +const StyledBox = styled(Flex, Box, { + position: 'relative', + borderRadius: '$12', + backgroundColor: 'white', + mt: '$14', + p: '$48', +}); + +const EmptyTeams = () => ( + + + + + + Create your first team + + {' '} + now. + + +); +export default EmptyTeams; diff --git a/frontend/src/components/Teams/TeamsList/partials/EmptyTeams/index.tsx b/frontend/src/components/Teams/TeamsList/partials/EmptyTeams/index.tsx deleted file mode 100644 index eacffa019..000000000 --- a/frontend/src/components/Teams/TeamsList/partials/EmptyTeams/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Link from 'next/link'; - -import { EmptyBoardsText, StyledBox, StyledImage, StyledNewTeamLink } from './styles'; - -const EmptyTeams: React.FC = () => ( - - - -
- - - Create your first team - - {' '} - now. -
-
-); -export default EmptyTeams; diff --git a/frontend/src/components/Teams/TeamsList/partials/EmptyTeams/styles.tsx b/frontend/src/components/Teams/TeamsList/partials/EmptyTeams/styles.tsx deleted file mode 100644 index 0f6eba95d..000000000 --- a/frontend/src/components/Teams/TeamsList/partials/EmptyTeams/styles.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { styled } from '@/styles/stitches/stitches.config'; - -import EmptyTeamsImage from '@/components/images/EmptyTeams'; -import Box from '@/components/Primitives/Box'; -import Flex from '@/components/Primitives/Flex'; -import Text from '@/components/Primitives/Text'; - -const StyledImage = styled(EmptyTeamsImage, Flex, Box, { '& svg': { zIndex: '-1' } }); - -const StyledBox = styled(Flex, Box, { - position: 'relative', - width: '100%', - mt: '$14', - backgroundColor: 'white', - pt: '$48', - borderRadius: '$12', -}); - -const EmptyBoardsText = styled(Text, { - mb: '48px', -}); - -const StyledNewTeamLink = styled('a', Text, { cursor: 'pointer' }); - -export { EmptyBoardsText, StyledBox, StyledImage, StyledNewTeamLink }; diff --git a/frontend/src/components/Teams/TeamsList/partials/ListOfCards/index.tsx b/frontend/src/components/Teams/TeamsList/partials/ListOfCards/index.tsx deleted file mode 100644 index 9eaf9c4bf..000000000 --- a/frontend/src/components/Teams/TeamsList/partials/ListOfCards/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -import Dots from '@/components/Primitives/Loading/Dots'; -import Flex from '@/components/Primitives/Flex'; -import { Team } from '@/types/team/team'; - -import { ScrollableContent } from '@/components/Boards/MyBoards/styles'; -import TeamItem from '@/components/Teams/TeamsList/TeamItem'; - -type ListOfCardsProp = { - teams: Team[]; - userId: string; - isLoading: boolean; -}; - -const ListOfCards = React.memo(({ teams, userId, isLoading }) => ( - - - {teams.map((team: Team) => ( - - ))} - - - {isLoading && ( - - - - )} - -)); - -export default ListOfCards; diff --git a/frontend/src/components/Teams/TeamsList/partials/ListOfCards/styles.tsx b/frontend/src/components/Teams/TeamsList/partials/ListOfCards/styles.tsx deleted file mode 100644 index 7cae40ecd..000000000 --- a/frontend/src/components/Teams/TeamsList/partials/ListOfCards/styles.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { styled } from '@/styles/stitches/stitches.config'; - -import Text from '@/components/Primitives/Text'; - -const LastUpdatedText = styled(Text, { - position: 'sticky', - zIndex: '5', - top: '-0.2px', - height: '$24', - backgroundColor: '$background', -}); - -export { LastUpdatedText }; diff --git a/frontend/src/components/Users/UserEdit/index.tsx b/frontend/src/components/Users/UserEdit/index.tsx index b4d1fb251..40a695ec8 100644 --- a/frontend/src/components/Users/UserEdit/index.tsx +++ b/frontend/src/components/Users/UserEdit/index.tsx @@ -3,12 +3,9 @@ import React from 'react'; import ListOfCards from './partials/ListOfCards'; type UserEditProps = { - userId: string | undefined; isLoading: boolean; }; -const UsersEdit = ({ userId, isLoading }: UserEditProps) => ( - -); +const UsersEdit = ({ isLoading }: UserEditProps) => ; export default UsersEdit; diff --git a/frontend/src/components/Users/UserEdit/partials/ListOfCards/index.tsx b/frontend/src/components/Users/UserEdit/partials/ListOfCards/index.tsx index 06d5dd2d1..57e697873 100644 --- a/frontend/src/components/Users/UserEdit/partials/ListOfCards/index.tsx +++ b/frontend/src/components/Users/UserEdit/partials/ListOfCards/index.tsx @@ -9,18 +9,17 @@ import { useRecoilValue } from 'recoil'; import { userTeamsListState } from '@/store/team/atom/team.atom'; type ListOfCardsProp = { - userId: string | undefined; isLoading: boolean; }; -const ListOfCards = React.memo(({ userId, isLoading }) => { +const ListOfCards = React.memo(({ isLoading }) => { const teamsOfUsers = useRecoilValue(userTeamsListState); return ( {teamsOfUsers?.map((team: Team) => ( - + ))} {isLoading && ( diff --git a/frontend/src/components/layouts/Layout/index.tsx b/frontend/src/components/layouts/Layout/index.tsx index d8069049f..358b2c9d6 100644 --- a/frontend/src/components/layouts/Layout/index.tsx +++ b/frontend/src/components/layouts/Layout/index.tsx @@ -5,7 +5,7 @@ import { signOut, useSession } from 'next-auth/react'; import Flex from '@/components/Primitives/Flex'; import LoadingPage from '@/components/Primitives/Loading/Page'; import Sidebar from '@/components/Sidebar'; -import { BOARDS_ROUTE, DASHBOARD_ROUTE, TEAMS_ROUTE, USERS_ROUTE } from '@/utils/routes'; +import { ROUTES } from '@/utils/routes'; import { REFRESH_TOKEN_ERROR } from '@/utils/constants'; import MainPageHeader from './partials/MainPageHeader'; import { Container, ContentSection } from './styles'; @@ -23,37 +23,37 @@ const Layout: React.FC<{ children: ReactNode }> = ({ children }) => { if (!session) return null; switch (router.pathname) { - case DASHBOARD_ROUTE: + case ROUTES.Dashboard: return ( ); - case BOARDS_ROUTE: + case ROUTES.Boards: return ( ); - case TEAMS_ROUTE: + case ROUTES.Teams: return ( ); - case USERS_ROUTE: + case ROUTES.Users: return ; default: return null; diff --git a/frontend/src/pages/teams/index.tsx b/frontend/src/pages/teams/index.tsx index 517f72433..e0c71d049 100644 --- a/frontend/src/pages/teams/index.tsx +++ b/frontend/src/pages/teams/index.tsx @@ -14,6 +14,7 @@ import requireAuthentication from '@/components/HOC/requireAuthentication'; import { useRecoilState } from 'recoil'; import { teamsListState } from '@/store/team/atom/team.atom'; import TeamsList from '@/components/Teams/TeamsList'; +import Dots from '@/components/Primitives/Loading/Dots'; const Teams = () => { const { data: session } = useSession({ required: true }); @@ -30,10 +31,24 @@ const Teams = () => { if (!session || !data) return null; return ( - + }> - + {isFetching ? ( + + + + ) : ( + + )} diff --git a/frontend/src/pages/users/[userId].tsx b/frontend/src/pages/users/[userId].tsx index 8f8e53585..848552feb 100644 --- a/frontend/src/pages/users/[userId].tsx +++ b/frontend/src/pages/users/[userId].tsx @@ -59,7 +59,7 @@ const UserDetails = () => { joinedAt={user.joinedAt} /> - {data && } + {data && }
diff --git a/frontend/src/stories/Teams/TeamCard.stories.tsx b/frontend/src/stories/Teams/TeamCard.stories.tsx index 9c4653497..9209c1846 100644 --- a/frontend/src/stories/Teams/TeamCard.stories.tsx +++ b/frontend/src/stories/Teams/TeamCard.stories.tsx @@ -30,7 +30,7 @@ const Template: ComponentStory = ({ team, isTeamPage }) => { createTeamUser(user, team); } - return ; + return ; }; export const Default = Template.bind({}); diff --git a/frontend/src/utils/routes.ts b/frontend/src/utils/routes.ts index d962fdbcf..16aa2c15e 100644 --- a/frontend/src/utils/routes.ts +++ b/frontend/src/utils/routes.ts @@ -8,19 +8,22 @@ export const ACCOUNT_ROUTE = '/account'; export const SETTINGS_ROUTE = '/settings'; export const ERROR_500_PAGE = '/500'; export const AZURE_LOGOUT_ROUTE = '/logoutAzure'; +export const LOGIN_GUEST_USER = '/login-guest-user'; export const ROUTES = { START_PAGE_ROUTE, Dashboard: DASHBOARD_ROUTE, Boards: BOARDS_ROUTE, - BoardPage: (boardId: string): string => `/boards/${boardId}`, + BoardPage: (boardId: string): string => `${BOARDS_ROUTE}/${boardId}`, + NewBoard: `${BOARDS_ROUTE}/new`, Token: RESET_PASSWORD_ROUTE, - TokenPage: (tokenId: string): string => `/reset-password/${tokenId}`, + TokenPage: (tokenId: string): string => `${RESET_PASSWORD_ROUTE}/${tokenId}`, Teams: TEAMS_ROUTE, - TeamPage: (teamId: string): string => `/teams/${teamId}`, + TeamPage: (teamId: string): string => `${TEAMS_ROUTE}/${teamId}`, + NewTeam: `${TEAMS_ROUTE}/new`, Users: USERS_ROUTE, - UserEdit: (userId: string) => `/users/${userId}`, - UserGuest: (boardId: string) => `/login-guest-user/${boardId}`, + UserEdit: (userId: string) => `${USERS_ROUTE}/${userId}`, + UserGuest: (boardId: string) => `${LOGIN_GUEST_USER}/${boardId}`, }; export const GetPageTitleByUrl = (url: string): string | undefined => diff --git a/frontend/src/utils/testing/mocks.ts b/frontend/src/utils/testing/mocks.ts index ad3bd8463..79aaf99d3 100644 --- a/frontend/src/utils/testing/mocks.ts +++ b/frontend/src/utils/testing/mocks.ts @@ -1,3 +1,4 @@ +import { User } from '@/types/user/user'; import { Session } from 'next-auth/core/types'; import { NextRouter } from 'next/router'; import { SessionUserFactory } from '../factories/user'; @@ -32,9 +33,9 @@ export function createMockRouter(router?: Partial): NextRouter { }; } -export function createMockSession(session?: Partial): Session { +export function createMockSession(session?: Partial, user?: User): Session { return { - user: SessionUserFactory.create({ isSAdmin: false }), + user: SessionUserFactory.create({ ...user, id: user?._id, isSAdmin: false }), expires: new Date().toISOString(), strategy: 'local', error: '', diff --git a/frontend/src/utils/testing/renderWithProviders.tsx b/frontend/src/utils/testing/renderWithProviders.tsx index 07c8c8c79..c4e91a688 100644 --- a/frontend/src/utils/testing/renderWithProviders.tsx +++ b/frontend/src/utils/testing/renderWithProviders.tsx @@ -1,3 +1,4 @@ +import { User } from '@/types/user/user'; import { createMockRouter, createMockSession } from '@/utils/testing/mocks'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { render as rtlRender, RenderOptions, RenderResult } from '@testing-library/react'; @@ -11,7 +12,10 @@ import { RecoilRoot } from 'recoil'; export type RenderWithProvidersOptions = Omit & { routerOptions?: Partial; queryClient?: QueryClient; - sessionOptions?: Session; + sessionOptions?: { + session?: Session; + user?: User; + }; }; export function renderWithProviders( @@ -22,7 +26,10 @@ export function renderWithProviders( wrapper: ({ children }: { children: ReactNode }) => { const router = createMockRouter(options?.routerOptions); const queryClient = options?.queryClient ?? new QueryClient(); - const session = createMockSession(options?.sessionOptions); + const session = createMockSession( + options?.sessionOptions?.session, + options?.sessionOptions?.user, + ); return (