From bd43e4620df4bc450c1fa8caac531a23d74cf4eb Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Mon, 12 May 2025 15:25:10 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84(teams)=20update=20team=20list=20pa?= =?UTF-8?q?ge=20to=20match=20new=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is an attempt to quick fix the team page to match the new UI. --- .../demo/tests/test_commands_create_demo.py | 4 +- .../components/TeamsListView.tsx | 70 +++++++++ .../__tests__/TeamsListView.test.tsx | 108 +++++++++++++ .../apps/desk/src/pages/teams/index.tsx | 144 +++++++++++++++--- .../apps/e2e/__tests__/app-desk/common.ts | 11 +- .../e2e/__tests__/app-desk/language.spec.ts | 8 +- .../app-desk/oidc-identity-provider.spec.ts | 4 +- .../__tests__/app-desk/teams-create.spec.ts | 22 +-- .../__tests__/app-desk/teams-panel.spec.ts | 50 +----- 9 files changed, 328 insertions(+), 93 deletions(-) create mode 100644 src/frontend/apps/desk/src/features/teams/team-management/components/TeamsListView.tsx create mode 100644 src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/TeamsListView.test.tsx diff --git a/src/backend/demo/tests/test_commands_create_demo.py b/src/backend/demo/tests/test_commands_create_demo.py index bb6a2f9..da86f03 100644 --- a/src/backend/demo/tests/test_commands_create_demo.py +++ b/src/backend/demo/tests/test_commands_create_demo.py @@ -40,7 +40,9 @@ def test_commands_create_demo(settings): assert models.Team.objects.count() == TEST_NB_OBJECTS["teams"] assert models.TeamAccess.objects.count() >= TEST_NB_OBJECTS["teams"] - assert mailbox_models.MailDomain.objects.count() == TEST_NB_OBJECTS["domains"] + 1 + 1 + assert ( + mailbox_models.MailDomain.objects.count() == TEST_NB_OBJECTS["domains"] + 1 + 1 + ) # 3 domain access for each user with domain rights # 3 x 3 domain access for each user with both rights diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/TeamsListView.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/TeamsListView.tsx new file mode 100644 index 0000000..abc92c8 --- /dev/null +++ b/src/frontend/apps/desk/src/features/teams/team-management/components/TeamsListView.tsx @@ -0,0 +1,70 @@ +import { Button, DataGrid } from '@openfun/cunningham-react'; +import { useRouter as useNavigate } from 'next/navigation'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { Text } from '@/components'; +import { TeamsOrdering, useTeams } from '@/features/teams/team-management'; + +interface TeamsListViewProps { + querySearch: string; +} + +export function TeamsListView({ querySearch }: TeamsListViewProps) { + const { t } = useTranslation(); + const router = useNavigate(); + const { data, isLoading } = useTeams({ + ordering: TeamsOrdering.BY_CREATED_ON_DESC, + }); + const teams = React.useMemo(() => data || [], [data]); + + const filteredTeams = React.useMemo(() => { + if (!querySearch) { + return teams; + } + const lower = querySearch.toLowerCase(); + return teams.filter((team) => team.name.toLowerCase().includes(lower)); + }, [teams, querySearch]); + + return ( +
+ {filteredTeams && filteredTeams.length ? ( + row.accesses.length, + }, + { + id: 'actions', + renderCell: ({ row }) => ( + + ), + }, + ]} + isLoading={isLoading} + /> + ) : null} + {(!filteredTeams || !filteredTeams.length) && ( + + {t('No teams exist.')} + + )} +
+ ); +} diff --git a/src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/TeamsListView.test.tsx b/src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/TeamsListView.test.tsx new file mode 100644 index 0000000..70efc5b --- /dev/null +++ b/src/frontend/apps/desk/src/features/teams/team-management/components/__tests__/TeamsListView.test.tsx @@ -0,0 +1,108 @@ +import { CunninghamProvider } from '@openfun/cunningham-react'; +import { render, screen } from '@testing-library/react'; +import { useRouter } from 'next/navigation'; +import { useTranslation } from 'react-i18next'; + +import { useTeams } from '@/features/teams/team-management'; + +import { TeamsListView } from '../TeamsListView'; + +// Mock the hooks +jest.mock('@/features/teams/team-management', () => ({ + useTeams: jest.fn(), + TeamsOrdering: { + BY_CREATED_ON_DESC: '-created_at', + }, +})); +jest.mock('next/navigation'); +jest.mock('react-i18next'); + +describe('TeamsListView', () => { + const mockTeams = [ + { + id: '1', + name: 'Team A', + accesses: [{ id: '1' }, { id: '2' }], + }, + { + id: '2', + name: 'Team B', + accesses: [{ id: '3' }], + }, + ]; + + const mockRouter = { + push: jest.fn(), + }; + + const renderWithProvider = (ui: React.ReactElement) => { + return render({ui}); + }; + + beforeEach(() => { + // Setup mock implementations + (useTeams as jest.Mock).mockReturnValue({ + data: mockTeams, + isLoading: false, + }); + (useRouter as jest.Mock).mockReturnValue(mockRouter); + (useTranslation as jest.Mock).mockReturnValue({ + t: (key: string) => key, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders teams list when teams exist', () => { + renderWithProvider(); + + // Check if team names are rendered + expect(screen.getByText('Team A')).toBeInTheDocument(); + expect(screen.getByText('Team B')).toBeInTheDocument(); + + // Check if member counts are rendered + expect(screen.getByText('2')).toBeInTheDocument(); // Team A has 2 members + expect(screen.getByText('1')).toBeInTheDocument(); // Team B has 1 member + }); + + it('shows "No teams exist" message when no teams are available', () => { + (useTeams as jest.Mock).mockReturnValue({ + data: [], + isLoading: false, + }); + + renderWithProvider(); + expect(screen.getByText('No teams exist.')).toBeInTheDocument(); + }); + + it('filters teams based on search query', () => { + renderWithProvider(); + + // Only Team A should be visible + expect(screen.getByText('Team A')).toBeInTheDocument(); + expect(screen.queryByText('Team B')).not.toBeInTheDocument(); + }); + + it('navigates to team details when clicking the view button', () => { + renderWithProvider(); + + // Find and click the view button for Team A + const viewButtons = screen.getAllByRole('button', { + name: 'View team details', + }); + viewButtons[0].click(); + + // Check if router.push was called with the correct path + expect(mockRouter.push).toHaveBeenCalledWith('/teams/1'); + }); + + it('handles case-insensitive search', () => { + renderWithProvider(); + + // Team A should still be visible despite different case + expect(screen.getByText('Team A')).toBeInTheDocument(); + expect(screen.queryByText('Team B')).not.toBeInTheDocument(); + }); +}); diff --git a/src/frontend/apps/desk/src/pages/teams/index.tsx b/src/frontend/apps/desk/src/pages/teams/index.tsx index 78472bc..66f1c2a 100644 --- a/src/frontend/apps/desk/src/pages/teams/index.tsx +++ b/src/frontend/apps/desk/src/pages/teams/index.tsx @@ -1,38 +1,146 @@ -import { Button } from '@openfun/cunningham-react'; +import { Button, Input, Tooltip } from '@openfun/cunningham-react'; import { useRouter as useNavigate } from 'next/navigation'; -import { ReactElement } from 'react'; +import React, { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; -import { Box, Text } from '@/components'; import { useAuthStore } from '@/core/auth'; -import { TeamLayout } from '@/features/teams/team-management'; +import { useCunninghamTheme } from '@/cunningham'; +import { TeamsListView } from '@/features/teams/team-management/components/TeamsListView'; +import { MainLayout } from '@/layouts'; import { NextPageWithLayout } from '@/types/next'; -const StyledButton = styled(Button)` - width: fit-content; -`; - const Page: NextPageWithLayout = () => { const { t } = useTranslation(); const router = useNavigate(); const { userData } = useAuthStore(); const can_create = userData?.abilities?.teams.can_create ?? false; + const [searchValue, setSearchValue] = React.useState(''); + const { colorsTokens } = useCunninghamTheme(); + const colors = colorsTokens(); + + const handleInputChange = (event: React.ChangeEvent) => { + setSearchValue(event.target.value); + }; + + const clearInput = () => { + setSearchValue(''); + }; return ( - - {can_create && ( - void router.push('/teams/create')}> - {t('Create a new team')} - - )} - {!can_create && {t('Click on team to view details')}} - +
+
+

+ {t('Teams')} +

+ +
+
+ search} + rightIcon={ + searchValue && ( + { + if (e.key === 'Enter' || e.key === ' ') { + clearInput(); + } + }} + role="button" + tabIndex={0} + style={{ cursor: 'pointer' }} + > + close + + ) + } + value={searchValue} + onChange={handleInputChange} + /> +
+ +
+ +
+ +
+ {can_create ? ( + + ) : ( + +
+ +
+
+ )} +
+
+ + {!can_create && ( +

+ {t('Click on team to view details')} +

+ )} + + +
+
); }; Page.getLayout = function getLayout(page: ReactElement) { - return {page}; + return {page}; }; export default Page; diff --git a/src/frontend/apps/e2e/__tests__/app-desk/common.ts b/src/frontend/apps/e2e/__tests__/app-desk/common.ts index f99a426..f91a479 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/common.ts @@ -43,12 +43,21 @@ export const createTeam = async ( length: number, ) => { const panel = page.getByLabel('Teams panel').first(); + const buttonCreateHomepage = page.getByRole('button', { + name: 'Create a new team', + }); const buttonCreate = page.getByRole('button', { name: 'Create the team' }); const randomTeams = randomName(teamName, browserName, length); for (let i = 0; i < randomTeams.length; i++) { - await panel.getByRole('button', { name: 'Add a team' }).click(); + if (i == 0) { + // for the first team, we need to click on the button in the homepage + await buttonCreateHomepage.click(); + } else { + // for the other teams, we need to click on the button in the panel of the detail view + await panel.getByRole('button', { name: 'Add a team' }).click(); + } await page.getByText('Team name').fill(randomTeams[i]); await expect(buttonCreate).toBeEnabled(); await buttonCreate.click(); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts index eba161d..951e195 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/language.spec.ts @@ -10,18 +10,14 @@ test.describe('Language', () => { }); test('checks the language picker', async ({ page }) => { - await expect( - page.getByLabel('Teams panel', { exact: true }).getByText('Groups'), - ).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Teams' })).toBeVisible(); const header = page.locator('header').first(); await header.getByRole('combobox').getByText('EN').click(); await header.getByRole('option', { name: 'FR' }).click(); await expect(header.getByRole('combobox').getByText('FR')).toBeVisible(); - await expect( - page.getByLabel('Teams panel', { exact: true }).getByText('Groupes'), - ).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Équipes' })).toBeVisible(); }); test('checks lang attribute of html tag updates when user changes language', async ({ diff --git a/src/frontend/apps/e2e/__tests__/app-desk/oidc-identity-provider.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/oidc-identity-provider.spec.ts index 4208d7c..847bbfc 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/oidc-identity-provider.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/oidc-identity-provider.spec.ts @@ -22,7 +22,7 @@ test.describe('Login to people as Identity Provider', () => { await expect(page).toHaveURL('http://localhost:3000/', { timeout: 10000 }); // check the user is logged in - await expect(page.getByText('Groups')).toBeVisible(); - await expect(page.getByText('0 group to display.')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Teams' })).toBeVisible(); + await expect(page.getByText('No teams exist.')).toBeVisible(); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts index c27975b..bc799d5 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-create.spec.ts @@ -61,9 +61,10 @@ test.describe('Teams Create', () => { page, browserName, }) => { - const panel = page.getByLabel('Teams panel').first(); - - await panel.getByRole('button', { name: 'Add a team' }).click(); + const buttonCreateHomepage = page.getByRole('button', { + name: 'Create a new team', + }); + await buttonCreateHomepage.click(); const teamName = `My routing team ${browserName}-${Math.floor(Math.random() * 1000)}`; await page.getByText('Team name').fill(teamName); @@ -72,6 +73,7 @@ test.describe('Teams Create', () => { const elTeam = page.getByRole('heading', { name: teamName }); await expect(elTeam).toBeVisible(); + const panel = page.getByLabel('Teams panel').first(); await panel.getByRole('button', { name: 'Add a team' }).click(); await expect(elTeam).toBeHidden(); @@ -79,20 +81,6 @@ test.describe('Teams Create', () => { await expect(elTeam).toBeVisible(); }); - test('checks alias teams url with homepage', async ({ page }) => { - await expect(page).toHaveURL('/'); - - const buttonCreateHomepage = page.getByRole('button', { - name: 'Create a new team', - }); - - await expect(buttonCreateHomepage).toBeVisible(); - - await page.goto('/teams/'); - await expect(buttonCreateHomepage).toBeVisible(); - await expect(page).toHaveURL(/\/teams\//); - }); - test('checks 404 on teams/[id] page', async ({ page }) => { await page.goto('/teams/some-unknown-team'); await expect( diff --git a/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts index 63266bd..61aba47 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/teams-panel.spec.ts @@ -9,55 +9,9 @@ test.beforeEach(async ({ page, browserName }) => { test.describe('Teams Panel', () => { test('checks all the elements are visible', async ({ page }) => { - const panel = page.getByLabel('Teams panel').first(); + await expect(page.getByRole('heading', { name: 'Teams' })).toBeVisible(); - await expect(panel.getByText('Groups')).toBeVisible(); - - await expect( - panel.getByRole('button', { - name: 'Sort the teams', - }), - ).toBeVisible(); - - await expect( - panel.getByRole('button', { - name: 'Add a team', - }), - ).toBeVisible(); - }); - - test('checks the sort button', async ({ page }) => { - const responsePromiseSortDesc = page.waitForResponse( - (response) => - response.url().includes('/teams/?ordering=-created_at') && - response.status() === 200, - ); - - const responsePromiseSortAsc = page.waitForResponse( - (response) => - response.url().includes('/teams/?ordering=created_at') && - response.status() === 200, - ); - - const panel = page.getByLabel('Teams panel').first(); - - await panel - .getByRole('button', { - name: 'Sort the teams by creation date ascendent', - }) - .click(); - - const responseSortAsc = await responsePromiseSortAsc; - expect(responseSortAsc.ok()).toBeTruthy(); - - await panel - .getByRole('button', { - name: 'Sort the teams by creation date descendent', - }) - .click(); - - const responseSortDesc = await responsePromiseSortDesc; - expect(responseSortDesc.ok()).toBeTruthy(); + await expect(page.getByTestId('button-new-team')).toBeVisible(); }); test('checks the hover and selected state', async ({ page, browserName }) => {