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 }) => {