💄(teams) update team list page to match new UI
This is an attempt to quick fix the team page to match the new UI.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<div role="listbox">
|
||||
{filteredTeams && filteredTeams.length ? (
|
||||
<DataGrid
|
||||
aria-label="listboxTeams"
|
||||
rows={filteredTeams}
|
||||
columns={[
|
||||
{
|
||||
field: 'name',
|
||||
headerName: t('Team'),
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: 'members',
|
||||
headerName: t('Member count'),
|
||||
enableSorting: false,
|
||||
renderCell: ({ row }) => row.accesses.length,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
renderCell: ({ row }) => (
|
||||
<Button
|
||||
color="tertiary"
|
||||
onClick={() => router.push(`/teams/${row.id}`)}
|
||||
aria-label={t('View team details')}
|
||||
>
|
||||
<span className="material-icons">chevron_right</span>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
) : null}
|
||||
{(!filteredTeams || !filteredTeams.length) && (
|
||||
<Text $align="center" $size="small">
|
||||
{t('No teams exist.')}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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(<CunninghamProvider>{ui}</CunninghamProvider>);
|
||||
};
|
||||
|
||||
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(<TeamsListView querySearch="" />);
|
||||
|
||||
// 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(<TeamsListView querySearch="" />);
|
||||
expect(screen.getByText('No teams exist.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters teams based on search query', () => {
|
||||
renderWithProvider(<TeamsListView querySearch="Team A" />);
|
||||
|
||||
// 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(<TeamsListView querySearch="" />);
|
||||
|
||||
// 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(<TeamsListView querySearch="team a" />);
|
||||
|
||||
// Team A should still be visible despite different case
|
||||
expect(screen.getByText('Team A')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Team B')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
setSearchValue(event.target.value);
|
||||
};
|
||||
|
||||
const clearInput = () => {
|
||||
setSearchValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Box $align="center" $justify="center" $height="inherit">
|
||||
{can_create && (
|
||||
<StyledButton onClick={() => void router.push('/teams/create')}>
|
||||
{t('Create a new team')}
|
||||
</StyledButton>
|
||||
)}
|
||||
{!can_create && <Text>{t('Click on team to view details')}</Text>}
|
||||
</Box>
|
||||
<div aria-label="Teams panel" className="container">
|
||||
<div
|
||||
data-testid="regie-grid"
|
||||
style={{
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
padding: '16px',
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
background: 'white',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${colorsTokens()['greyscale-200']}`,
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
style={{ fontWeight: 700, fontSize: '1.5rem', marginBottom: '20px' }}
|
||||
>
|
||||
{t('Teams')}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="sm:block md:flex"
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '20px',
|
||||
gap: '1em',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ width: 'calc(100% - 245px)' }}
|
||||
className="c__input__wrapper__mobile"
|
||||
>
|
||||
<Input
|
||||
style={{ width: '100%' }}
|
||||
label={t('Search a team')}
|
||||
icon={<span className="material-icons">search</span>}
|
||||
rightIcon={
|
||||
searchValue && (
|
||||
<span
|
||||
className="material-icons"
|
||||
onClick={clearInput}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
clearInput();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
close
|
||||
</span>
|
||||
)
|
||||
}
|
||||
value={searchValue}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="hidden md:flex"
|
||||
style={{
|
||||
background: colors['greyscale-200'],
|
||||
height: '32px',
|
||||
width: '1px',
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div
|
||||
className="block md:hidden"
|
||||
style={{ marginBottom: '10px' }}
|
||||
></div>
|
||||
|
||||
<div>
|
||||
{can_create ? (
|
||||
<Button
|
||||
data-testid="button-new-team"
|
||||
onClick={() => void router.push('/teams/create')}
|
||||
>
|
||||
{t('Create a new team')}
|
||||
</Button>
|
||||
) : (
|
||||
<Tooltip content="You don't have the correct access right">
|
||||
<div>
|
||||
<Button
|
||||
data-testid="button-new-team"
|
||||
onClick={() => void router.push('/teams/create')}
|
||||
disabled={!can_create}
|
||||
>
|
||||
{t('Create a new team')}
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!can_create && (
|
||||
<p style={{ textAlign: 'center' }}>
|
||||
{t('Click on team to view details')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<TeamsListView querySearch={searchValue} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = function getLayout(page: ReactElement) {
|
||||
return <TeamLayout>{page}</TeamLayout>;
|
||||
return <MainLayout backgroundColor="grey">{page}</MainLayout>;
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
Reference in New Issue
Block a user