mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +00:00
chore(helper-plugin): deprecate MORE components (#19718)
This commit is contained in:
parent
ab2af1e539
commit
af3c6ff7bd
348
packages/core/admin/admin/src/components/Pagination.tsx
Normal file
348
packages/core/admin/admin/src/components/Pagination.tsx
Normal file
@ -0,0 +1,348 @@
|
||||
/* eslint-disable import/export */
|
||||
import * as React from 'react';
|
||||
|
||||
import { Flex, SingleSelectOption, SingleSelect, Typography } from '@strapi/design-system';
|
||||
import {
|
||||
Dots,
|
||||
NextLink,
|
||||
PageLink,
|
||||
Pagination as PaginationImpl,
|
||||
PreviousLink,
|
||||
} from '@strapi/design-system/v2';
|
||||
import { useQueryParams } from '@strapi/helper-plugin';
|
||||
import { stringify } from 'qs';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { createContext } from './Context';
|
||||
|
||||
import type { Pagination as PaginationApi } from '../../../shared/contracts/shared';
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Root
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
interface PaginationContextValue {
|
||||
/**
|
||||
* @description the complete query object, this could include query params
|
||||
* injected by other plugins, if you're navigating to a different page you
|
||||
* should ensure these are still passed.
|
||||
*/
|
||||
currentQuery?: object;
|
||||
pageCount: string;
|
||||
pageSize: string;
|
||||
page: string;
|
||||
setPageSize: (pageSize: string) => void;
|
||||
total: NonNullable<RootProps['total']>;
|
||||
}
|
||||
|
||||
const [PaginationProvider, usePagination] = createContext<PaginationContextValue>('Pagination');
|
||||
|
||||
interface RootProps {
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* @default 0
|
||||
* @description the total number of pages
|
||||
* that exist in the dataset.
|
||||
*/
|
||||
pageCount?: PaginationApi['pageCount'];
|
||||
/**
|
||||
* @default 1
|
||||
* @description the initial page number.
|
||||
*/
|
||||
defaultPage?: PaginationApi['page'];
|
||||
/**
|
||||
* @default 10
|
||||
* @description the initial number of items to display
|
||||
*/
|
||||
defaultPageSize?: PaginationApi['pageSize'];
|
||||
/**
|
||||
* @description a callback that is called when the page size changes.
|
||||
*/
|
||||
onPageSizeChange?: (pageSize: string) => void;
|
||||
/**
|
||||
* @default 0
|
||||
* @description the total number of items in the dataset.
|
||||
*/
|
||||
total?: PaginationApi['total'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description The root component for the composable pagination component.
|
||||
* It's advised to spread the entire pagination option object into this component.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const MyComponent = () => {
|
||||
* return (
|
||||
* <Pagination.Root {...response.pagination}>
|
||||
* <Pagination.PageSize />
|
||||
* <Pagination.Links />
|
||||
* </Pagination.Root>
|
||||
* );
|
||||
* };
|
||||
* ```
|
||||
*/
|
||||
const Root = React.forwardRef<HTMLDivElement, RootProps>(
|
||||
(
|
||||
{ children, defaultPageSize = 10, pageCount = 0, defaultPage = 1, onPageSizeChange, total = 0 },
|
||||
forwardedRef
|
||||
) => {
|
||||
const [{ query }, setQuery] = useQueryParams<Pick<PaginationContextValue, 'page' | 'pageSize'>>(
|
||||
{
|
||||
pageSize: defaultPageSize.toString(),
|
||||
page: defaultPage.toString(),
|
||||
}
|
||||
);
|
||||
|
||||
const setPageSize = (pageSize: string) => {
|
||||
setQuery({ pageSize, page: '1' });
|
||||
|
||||
if (onPageSizeChange) {
|
||||
onPageSizeChange(pageSize);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex ref={forwardedRef} paddingTop={4} alignItems="flex-end" justifyContent="space-between">
|
||||
<PaginationProvider
|
||||
currentQuery={query}
|
||||
page={query.page}
|
||||
pageSize={query.pageSize}
|
||||
pageCount={pageCount.toString()}
|
||||
setPageSize={setPageSize}
|
||||
total={total}
|
||||
>
|
||||
{children}
|
||||
</PaginationProvider>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* PageSize
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @description The page size component is responsible for rendering the select input that allows
|
||||
* the user to change the number of items displayed per page.
|
||||
* If the total number of items is less than the minimum option, this component will not render.
|
||||
*/
|
||||
const PageSize = ({ options = ['10', '20', '50', '100'] }: Pagination.PageSizeProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const pageSize = usePagination('PageSize', (state) => state.pageSize);
|
||||
const totalCount = usePagination('PageSize', (state) => state.total);
|
||||
const setPageSize = usePagination('PageSize', (state) => state.setPageSize);
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setPageSize(value);
|
||||
};
|
||||
|
||||
const minimumOption = parseInt(options[0], 10);
|
||||
|
||||
if (minimumOption >= totalCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<SingleSelect
|
||||
size="S"
|
||||
aria-label={formatMessage({
|
||||
id: 'components.PageFooter.select',
|
||||
defaultMessage: 'Entries per page',
|
||||
})}
|
||||
// @ts-expect-error from the DS V2 this won't be needed because we're only returning strings.
|
||||
onChange={handleChange}
|
||||
value={pageSize}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<SingleSelectOption key={option} value={option}>
|
||||
{option}
|
||||
</SingleSelectOption>
|
||||
))}
|
||||
</SingleSelect>
|
||||
<Typography textColor="neutral600" as="span">
|
||||
{formatMessage({
|
||||
id: 'components.PageFooter.select',
|
||||
defaultMessage: 'Entries per page',
|
||||
})}
|
||||
</Typography>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Links
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* The component works as follows
|
||||
* `1` , 2, 3, ... 10
|
||||
* 1, `2`, 3, ... 10
|
||||
* 1, 2, `3`, 4, ... 10
|
||||
* 1, 2, 3, `4`, 5, ... 10
|
||||
* 1, ..,4, `5`, 6, ... 10
|
||||
*
|
||||
* 1, ...., 8, 9, `10`
|
||||
* 1, ...., 8, `9`, 10
|
||||
* 1, ...., 7, `8`, 9, 10
|
||||
* 1, ... 6, `7`, 8, 9, 10
|
||||
*/
|
||||
|
||||
/**
|
||||
* @description The links component is responsible for rendering the pagination links.
|
||||
* If the total number of pages is less than or equal to 1, this component will not render.
|
||||
*/
|
||||
const Links = ({ boundaryCount = 1, siblingCount = 1 }: Pagination.LinksProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const query = usePagination('Links', (state) => state.currentQuery);
|
||||
const currentPage = usePagination('Links', (state) => state.page);
|
||||
const totalPages = usePagination('Links', (state) => state.pageCount);
|
||||
|
||||
const pageCount = parseInt(totalPages, 10);
|
||||
const activePage = parseInt(currentPage, 10);
|
||||
|
||||
const range = (start: number, end: number) => {
|
||||
const length = end - start + 1;
|
||||
|
||||
return Array.from({ length }, (_, i) => start + i);
|
||||
};
|
||||
|
||||
const startPages = range(1, Math.min(boundaryCount, pageCount));
|
||||
const endPages = range(Math.max(pageCount - boundaryCount + 1, boundaryCount + 1), pageCount);
|
||||
|
||||
const siblingsStart = Math.max(
|
||||
Math.min(
|
||||
// Natural start
|
||||
activePage - siblingCount,
|
||||
// Lower boundary when page is high
|
||||
pageCount - boundaryCount - siblingCount * 2 - 1
|
||||
),
|
||||
// Greater than startPages
|
||||
boundaryCount + 2
|
||||
);
|
||||
|
||||
const siblingsEnd = Math.min(
|
||||
Math.max(
|
||||
// Natural end
|
||||
activePage + siblingCount,
|
||||
// Upper boundary when page is low
|
||||
boundaryCount + siblingCount * 2 + 2
|
||||
),
|
||||
// Less than endPages
|
||||
endPages.length > 0 ? endPages[0] - 2 : pageCount - 1
|
||||
);
|
||||
|
||||
const items = [
|
||||
...startPages,
|
||||
|
||||
// Start ellipsis
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
...(siblingsStart > boundaryCount + 2
|
||||
? ['start-ellipsis']
|
||||
: boundaryCount + 1 < pageCount - boundaryCount
|
||||
? [boundaryCount + 1]
|
||||
: []),
|
||||
|
||||
// Sibling pages
|
||||
...range(siblingsStart, siblingsEnd),
|
||||
|
||||
// End ellipsis
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
...(siblingsEnd < pageCount - boundaryCount - 1
|
||||
? ['end-ellipsis']
|
||||
: pageCount - boundaryCount > boundaryCount
|
||||
? [pageCount - boundaryCount]
|
||||
: []),
|
||||
|
||||
...endPages,
|
||||
];
|
||||
|
||||
if (pageCount <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PaginationImpl activePage={activePage} pageCount={pageCount}>
|
||||
<PreviousLink
|
||||
as={Link}
|
||||
// @ts-expect-error – the `as` prop does not correctly infer the props of it's component
|
||||
to={{ search: stringify({ ...query, page: activePage - 1 }) }}
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'components.pagination.go-to-previous',
|
||||
defaultMessage: 'Go to previous page',
|
||||
})}
|
||||
</PreviousLink>
|
||||
{items.map((item) => {
|
||||
if (typeof item === 'number') {
|
||||
return (
|
||||
<PageLink
|
||||
as={Link}
|
||||
active={item === activePage}
|
||||
key={item}
|
||||
number={item}
|
||||
// @ts-expect-error – the `as` prop does not correctly infer the props of it's component
|
||||
to={{ search: stringify({ ...query, page: item }) }}
|
||||
>
|
||||
{formatMessage(
|
||||
{ id: 'components.pagination.go-to', defaultMessage: 'Go to page {page}' },
|
||||
{ page: item }
|
||||
)}
|
||||
</PageLink>
|
||||
);
|
||||
}
|
||||
|
||||
return <Dots key={item} />;
|
||||
})}
|
||||
|
||||
<NextLink
|
||||
as={Link}
|
||||
// @ts-expect-error – the `as` prop does not correctly infer the props of it's component
|
||||
to={{ search: stringify({ ...query, page: activePage + 1 }) }}
|
||||
>
|
||||
{formatMessage({
|
||||
id: 'components.pagination.go-to-next',
|
||||
defaultMessage: 'Go to next page',
|
||||
})}
|
||||
</NextLink>
|
||||
</PaginationImpl>
|
||||
);
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* EXPORTS
|
||||
* -----------------------------------------------------------------------------------------------*/
|
||||
|
||||
const Pagination = {
|
||||
Root,
|
||||
Links,
|
||||
PageSize,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Pagination {
|
||||
export interface Props extends RootProps {}
|
||||
|
||||
export interface PageSizeProps {
|
||||
options?: string[];
|
||||
}
|
||||
|
||||
export interface LinksProps {
|
||||
/**
|
||||
* @default 1
|
||||
* @description Number of always visible pages at the beginning and end.
|
||||
*/
|
||||
boundaryCount?: number;
|
||||
/**
|
||||
* @default 1
|
||||
* @description Number of always visible pages before and after the current page.
|
||||
*/
|
||||
siblingCount?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export { Pagination };
|
||||
76
packages/core/admin/admin/src/components/RelativeTime.tsx
Normal file
76
packages/core/admin/admin/src/components/RelativeTime.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Duration, intervalToDuration, isPast } from 'date-fns';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const intervals: Array<keyof Duration> = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
|
||||
|
||||
interface CustomInterval {
|
||||
unit: keyof Duration;
|
||||
text: string;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
interface RelativeTimeProps extends React.ComponentPropsWithoutRef<'time'> {
|
||||
timestamp: Date;
|
||||
customIntervals?: CustomInterval[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the relative time between a given timestamp and the current time.
|
||||
* You can display a custom message for given time intervals by passing an array of custom intervals.
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <caption>Display "last hour" if the timestamp is less than an hour ago</caption>
|
||||
* <RelativeTime
|
||||
* timestamp={new Date('2021-01-01')}
|
||||
* customIntervals={[
|
||||
* { unit: 'hours', threshold: 1, text: 'last hour' },
|
||||
* ]}
|
||||
* ```
|
||||
*/
|
||||
const RelativeTime = React.forwardRef<HTMLTimeElement, RelativeTimeProps>(
|
||||
({ timestamp, customIntervals = [], ...restProps }, forwardedRef) => {
|
||||
const { formatRelativeTime, formatDate, formatTime } = useIntl();
|
||||
|
||||
/**
|
||||
* TODO: make this auto-update, like a clock.
|
||||
*/
|
||||
const interval = intervalToDuration({
|
||||
start: timestamp,
|
||||
end: Date.now(),
|
||||
// see https://github.com/date-fns/date-fns/issues/2891 – No idea why it's all partial it returns it every time.
|
||||
}) as Required<Duration>;
|
||||
|
||||
const unit = intervals.find((intervalUnit) => {
|
||||
return interval[intervalUnit] > 0 && Object.keys(interval).includes(intervalUnit);
|
||||
})!;
|
||||
|
||||
const relativeTime = isPast(timestamp) ? -interval[unit] : interval[unit];
|
||||
|
||||
// Display custom text if interval is less than the threshold
|
||||
const customInterval = customIntervals.find(
|
||||
(custom) => interval[custom.unit] < custom.threshold
|
||||
);
|
||||
|
||||
const displayText = customInterval
|
||||
? customInterval.text
|
||||
: formatRelativeTime(relativeTime, unit, { numeric: 'auto' });
|
||||
|
||||
return (
|
||||
<time
|
||||
ref={forwardedRef}
|
||||
dateTime={timestamp.toISOString()}
|
||||
role="time"
|
||||
title={`${formatDate(timestamp)} ${formatTime(timestamp)}`}
|
||||
{...restProps}
|
||||
>
|
||||
{displayText}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { RelativeTime };
|
||||
export type { CustomInterval, RelativeTimeProps };
|
||||
@ -1,13 +1,11 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Icon, IconButton, Searchbar, SearchForm } from '@strapi/design-system';
|
||||
import { useQueryParams, type TrackingEvent, useTracking } from '@strapi/helper-plugin';
|
||||
import { Search as SearchIcon } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { TrackingEvent, useTracking } from '../features/Tracking';
|
||||
import { useQueryParams } from '../hooks/useQueryParams';
|
||||
|
||||
export interface SearchURLQueryProps {
|
||||
interface SearchInputProps {
|
||||
disabled?: boolean;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
@ -15,13 +13,13 @@ export interface SearchURLQueryProps {
|
||||
trackedEventDetails?: TrackingEvent['properties'];
|
||||
}
|
||||
|
||||
const SearchURLQuery = ({
|
||||
const SearchInput = ({
|
||||
disabled,
|
||||
label,
|
||||
placeholder,
|
||||
trackedEvent,
|
||||
trackedEventDetails,
|
||||
}: SearchURLQueryProps) => {
|
||||
}: SearchInputProps) => {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const iconButtonRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
@ -67,7 +65,7 @@ const SearchURLQuery = ({
|
||||
<Searchbar
|
||||
ref={inputRef}
|
||||
name="search"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
value={value}
|
||||
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
|
||||
onClear={handleClear}
|
||||
@ -91,4 +89,5 @@ const SearchURLQuery = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { SearchURLQuery };
|
||||
export { SearchInput };
|
||||
export type { SearchInputProps };
|
||||
@ -0,0 +1,266 @@
|
||||
import { RenderOptions, render as renderRTL, screen, waitFor } from '@tests/utils';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { Pagination } from '../Pagination';
|
||||
|
||||
const LocationDisplay = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li data-testId="location">{location.search}</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
const render = ({
|
||||
initialEntries,
|
||||
options,
|
||||
...props
|
||||
}: Partial<Pagination.Props> & Pagination.PageSizeProps & RenderOptions = {}) =>
|
||||
renderRTL(
|
||||
<Pagination.Root pageCount={2} total={100} defaultPageSize={50} {...props}>
|
||||
<Pagination.PageSize options={options} />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>,
|
||||
{
|
||||
initialEntries,
|
||||
renderOptions: {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<LocationDisplay />
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
describe('Pagination', () => {
|
||||
it("doesn't render anything when there is not enough pagination data", () => {
|
||||
render({ pageCount: 0, total: 0 });
|
||||
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the complete pagination component when there is enough pagination data', () => {
|
||||
render();
|
||||
|
||||
expect(screen.getByRole('combobox')).toHaveTextContent('50');
|
||||
expect(screen.getByText('Entries per page')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
expect(screen.getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
});
|
||||
|
||||
describe('PageSize', () => {
|
||||
it('should display the pageSize correctly if its in the url query', () => {
|
||||
render({
|
||||
initialEntries: [{ search: 'pageSize=50' }],
|
||||
});
|
||||
|
||||
expect(screen.getByRole('combobox')).toHaveTextContent('50');
|
||||
});
|
||||
|
||||
it('should render a custom list of options if provided', async () => {
|
||||
const options = ['5', '10', '15'];
|
||||
|
||||
const { user } = render({ options, defaultPageSize: 10 });
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('combobox')).toHaveTextContent('10'));
|
||||
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
|
||||
options.forEach((option) => {
|
||||
expect(screen.getByRole('option', { name: option })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interaction', () => {
|
||||
it('should change the value when the user selects a new value', async () => {
|
||||
const { user } = render();
|
||||
|
||||
await user.click(screen.getByRole('combobox'));
|
||||
|
||||
await user.click(screen.getByRole('option', { name: '20' }));
|
||||
|
||||
expect(screen.getByRole('combobox')).toHaveTextContent('20');
|
||||
|
||||
const searchParams = new URLSearchParams(screen.getByTestId('location').textContent ?? '');
|
||||
|
||||
expect(searchParams.has('pageSize')).toBe(true);
|
||||
expect(searchParams.get('pageSize')).toBe('20');
|
||||
});
|
||||
|
||||
it('should use the default value and then change the value when the user selects a new value', async () => {
|
||||
const { getByRole, user } = render({ defaultPageSize: 20 });
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('20');
|
||||
|
||||
await user.click(getByRole('combobox'));
|
||||
|
||||
await user.click(getByRole('option', { name: '50' }));
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('50');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Links', () => {
|
||||
it('should display 4 links when the pageCount is greater than 4', () => {
|
||||
render({
|
||||
pageCount: 4,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 4' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render only 4 links when the page count is 5 or greater', () => {
|
||||
const { rerender } = render({ pageCount: 5 });
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 5' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
|
||||
rerender(
|
||||
<Pagination.Root total={100} pageCount={10}>
|
||||
<Pagination.PageSize />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 10' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render multiple dot elements when the page count is greater than 5 and the current page is greater than 4', () => {
|
||||
render({
|
||||
initialEntries: [{ pathname: '/', search: 'page=5' }],
|
||||
pageCount: 10,
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 4' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 5' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 6' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to page 10' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set the default page to the same as the query', () => {
|
||||
render({
|
||||
pageCount: 4,
|
||||
initialEntries: [{ pathname: '/', search: 'page=3' }],
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('page=3');
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
});
|
||||
|
||||
it('should change the page correctly when the user clicks on a page link', async () => {
|
||||
const { user } = render({
|
||||
pageCount: 4,
|
||||
initialEntries: [{ pathname: '/', search: 'page=3' }],
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('link', { name: 'Go to page 2' }));
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('page=2');
|
||||
});
|
||||
|
||||
it('should change the page correctly when the user clicks on the arrows', async () => {
|
||||
const { user } = render({
|
||||
pageCount: 4,
|
||||
initialEntries: [{ pathname: '/', search: 'page=3' }],
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('link', { name: 'Go to previous page' }));
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('page=2');
|
||||
|
||||
await user.click(screen.getByRole('link', { name: 'Go to next page' }));
|
||||
await user.click(screen.getByRole('link', { name: 'Go to next page' }));
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 4' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('page=4');
|
||||
});
|
||||
|
||||
it('should carry the rest of the query params when the user clicks on a page link', async () => {
|
||||
const { user } = render({
|
||||
pageCount: 4,
|
||||
initialEntries: [{ pathname: '/', search: 'page=3&search=foo' }],
|
||||
});
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole('link', { name: 'Go to page 2' }));
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location')).toHaveTextContent('page=2');
|
||||
|
||||
expect(screen.getByRole('link', { name: 'Go to page 4' })).toHaveAttribute(
|
||||
'href',
|
||||
'/?pageSize=50&page=4&search=foo'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
import { render, screen } from '@tests/utils';
|
||||
|
||||
import { RelativeTime } from '../RelativeTime';
|
||||
|
||||
const setDateNow = (now: number): jest.Spied<typeof Date.now> =>
|
||||
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||
|
||||
let spiedDateNow: ReturnType<typeof setDateNow> | undefined = undefined;
|
||||
|
||||
describe('RelativeTime', () => {
|
||||
afterAll(() => {
|
||||
if (spiedDateNow) {
|
||||
spiedDateNow.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders and matches the snapshot', () => {
|
||||
spiedDateNow = setDateNow(1443686400000); // 2015-10-01 08:00:00
|
||||
render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(screen.getByRole('time')).toMatchInlineSnapshot(`
|
||||
<time
|
||||
datetime="2015-10-01T07:55:00.000Z"
|
||||
role="time"
|
||||
title="10/1/2015 7:55 AM"
|
||||
>
|
||||
5 minutes ago
|
||||
</time>
|
||||
`);
|
||||
});
|
||||
|
||||
it('can display the relative time for a future date', () => {
|
||||
spiedDateNow = setDateNow(1443685800000); // 2015-10-01 07:50:00
|
||||
render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(screen.getByRole('time')).toHaveTextContent('in 5 minutes');
|
||||
// expect(getByText('in 5 minutes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can display the relative time for a past date', () => {
|
||||
spiedDateNow = setDateNow(1443686400000); // 2015-10-01 08:00:00
|
||||
render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(screen.getByRole('time')).toHaveTextContent('5 minutes ago');
|
||||
// expect(getByText('5 minutes ago')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -1,14 +1,7 @@
|
||||
import { render } from '@tests/utils';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { SearchURLQuery } from '../SearchURLQuery';
|
||||
|
||||
const trackUsage = jest.fn();
|
||||
jest.mock('../../features/Tracking', () => ({
|
||||
useTracking: () => ({
|
||||
trackUsage,
|
||||
}),
|
||||
}));
|
||||
import { SearchInput } from '../SearchInput';
|
||||
|
||||
const LocationDisplay = () => {
|
||||
const location = useLocation();
|
||||
@ -20,15 +13,15 @@ const LocationDisplay = () => {
|
||||
);
|
||||
};
|
||||
|
||||
describe('<SearchURLQuery />', () => {
|
||||
describe('SearchInput', () => {
|
||||
it('should render an icon button by default', () => {
|
||||
const { getByRole } = render(<SearchURLQuery label="Search label" />);
|
||||
const { getByRole } = render(<SearchInput label="Search label" />);
|
||||
|
||||
expect(getByRole('button', { name: 'Search' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should toggle searchbar form and searchbar', async () => {
|
||||
const { user, getByRole } = render(<SearchURLQuery label="Search label" />);
|
||||
const { user, getByRole } = render(<SearchInput label="Search label" />);
|
||||
|
||||
await user.click(getByRole('button', { name: 'Search' }));
|
||||
|
||||
@ -36,7 +29,7 @@ describe('<SearchURLQuery />', () => {
|
||||
});
|
||||
|
||||
it('should push value to query params', async () => {
|
||||
const { user, getByRole } = render(<SearchURLQuery label="Search label" />, {
|
||||
const { user, getByRole } = render(<SearchInput label="Search label" />, {
|
||||
renderOptions: {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
@ -63,7 +56,7 @@ describe('<SearchURLQuery />', () => {
|
||||
});
|
||||
|
||||
it('should clear value and update query params', async () => {
|
||||
const { user, getByRole } = render(<SearchURLQuery label="Search label" />, {
|
||||
const { user, getByRole } = render(<SearchInput label="Search label" />, {
|
||||
renderOptions: {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
@ -90,16 +83,4 @@ describe('<SearchURLQuery />', () => {
|
||||
|
||||
expect(new URLSearchParams(getByRole('listitem').textContent ?? '').has('_q')).toBe(false);
|
||||
});
|
||||
|
||||
it('should call trackUsage with trackedEvent props when submit', async () => {
|
||||
const { getByRole, user } = render(
|
||||
<SearchURLQuery label="Search label" trackedEvent="didSearch" />
|
||||
);
|
||||
|
||||
await user.click(getByRole('button', { name: 'Search' }));
|
||||
await user.type(getByRole('textbox', { name: 'Search label' }), 'michka');
|
||||
await user.keyboard('[Enter]');
|
||||
|
||||
expect(trackUsage.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Flex, Typography, type BoxProps } from '@strapi/design-system';
|
||||
import { RelativeTime, useQueryParams } from '@strapi/helper-plugin';
|
||||
import { useQueryParams } from '@strapi/helper-plugin';
|
||||
import { stringify } from 'qs';
|
||||
import { type MessageDescriptor, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { RelativeTime } from '../../../components/RelativeTime';
|
||||
import { getDisplayName } from '../../utils/users';
|
||||
import { useHistoryContext } from '../pages/History';
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
Typography,
|
||||
} from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import { RelativeTime, useNotification, useQueryParams, useStrapiApp } from '@strapi/helper-plugin';
|
||||
import { useNotification, useQueryParams, useStrapiApp } from '@strapi/helper-plugin';
|
||||
import { ArrowLeft, Cog, ExclamationMarkCircle, Pencil, Trash } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useMatch, useNavigate } from 'react-router-dom';
|
||||
@ -17,6 +17,7 @@ import styled from 'styled-components';
|
||||
|
||||
import { DescriptionComponentRenderer } from '../../../../components/DescriptionComponentRenderer';
|
||||
import { useForm } from '../../../../components/Form';
|
||||
import { RelativeTime } from '../../../../components/RelativeTime';
|
||||
import { capitalise } from '../../../../utils/strings';
|
||||
import {
|
||||
CREATED_AT_ATTRIBUTE_NAME,
|
||||
|
||||
@ -16,15 +16,12 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import {
|
||||
SearchURLQuery,
|
||||
useFocusWhenNavigate,
|
||||
useQueryParams,
|
||||
useNotification,
|
||||
useTracking,
|
||||
useAPIErrorHandler,
|
||||
useStrapiApp,
|
||||
PaginationURLQuery,
|
||||
PageSizeURLQuery,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { ArrowLeft, Plus } from '@strapi/icons';
|
||||
import { stringify } from 'qs';
|
||||
@ -34,6 +31,8 @@ import styled from 'styled-components';
|
||||
|
||||
import { InjectionZone } from '../../../components/InjectionZone';
|
||||
import { Page } from '../../../components/PageHelpers';
|
||||
import { Pagination } from '../../../components/Pagination';
|
||||
import { SearchInput } from '../../../components/SearchInput';
|
||||
import { HOOKS } from '../../../constants';
|
||||
import { useEnterprise } from '../../../hooks/useEnterprise';
|
||||
import { capitalise } from '../../../utils/strings';
|
||||
@ -303,7 +302,7 @@ const ListViewPage = () => {
|
||||
startActions={
|
||||
<>
|
||||
{list.settings.searchable && (
|
||||
<SearchURLQuery
|
||||
<SearchInput
|
||||
disabled={results.length === 0}
|
||||
label={formatMessage(
|
||||
{ id: 'app.component.search.label', defaultMessage: 'Search for {target}' },
|
||||
@ -459,10 +458,13 @@ const ListViewPage = () => {
|
||||
</Table.Body>
|
||||
</Table.Content>
|
||||
</Table.Root>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery trackedEvent="willChangeNumberOfEntriesPerPage" />
|
||||
<PaginationURLQuery pagination={{ pageCount: pagination?.pageCount || 1 }} />
|
||||
</Flex>
|
||||
<Pagination.Root
|
||||
{...pagination}
|
||||
onPageSizeChange={() => trackUsage('willChangeNumberOfEntriesPerPage')}
|
||||
>
|
||||
<Pagination.PageSize />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
</Flex>
|
||||
</ContentLayout>
|
||||
</Main>
|
||||
|
||||
@ -10,6 +10,8 @@ export * from './render';
|
||||
export * from './components/Form';
|
||||
export * from './components/FormInputs/Renderer';
|
||||
export * from './components/PageHelpers';
|
||||
export * from './components/Pagination';
|
||||
export * from './components/SearchInput';
|
||||
|
||||
/**
|
||||
* Hooks
|
||||
|
||||
@ -15,8 +15,6 @@ import {
|
||||
Tabs,
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
PageSizeURLQuery,
|
||||
PaginationURLQuery,
|
||||
useAppInfo,
|
||||
useFocusWhenNavigate,
|
||||
useNotification,
|
||||
@ -29,6 +27,7 @@ import { useIntl } from 'react-intl';
|
||||
|
||||
import { ContentBox } from '../../components/ContentBox';
|
||||
import { Page } from '../../components/PageHelpers';
|
||||
import { Pagination } from '../../components/Pagination';
|
||||
import { useTypedSelector } from '../../core/store/hooks';
|
||||
import { useDebounce } from '../../hooks/useDebounce';
|
||||
|
||||
@ -269,14 +268,10 @@ const MarketplacePage = () => {
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
{pagination ? (
|
||||
<Box paddingTop={4}>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery options={['12', '24', '50', '100']} defaultValue="24" />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Flex>
|
||||
</Box>
|
||||
) : null}
|
||||
<Pagination.Root {...pagination} defaultPageSize={24}>
|
||||
<Pagination.PageSize options={['12', '24', '50', '100']} />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
<Box paddingTop={8}>
|
||||
<a
|
||||
href="https://strapi.canny.io/plugin-requests"
|
||||
|
||||
@ -28,7 +28,7 @@ const waitForReload = async () => {
|
||||
const LocationDisplay = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return <span>{location.search}</span>;
|
||||
return <span data-testId="location">{location.search}</span>;
|
||||
};
|
||||
|
||||
const render = () =>
|
||||
@ -330,7 +330,7 @@ describe('Marketplace page - plugins tab', () => {
|
||||
|
||||
await waitForReload();
|
||||
|
||||
expect(screen.getByText('?page=1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(`"?page=1"`);
|
||||
});
|
||||
|
||||
it('only filters in the plugins tab', async () => {
|
||||
@ -378,7 +378,9 @@ describe('Marketplace page - plugins tab', () => {
|
||||
await user.click(getByRole('option', { name: 'Newest' }));
|
||||
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?sort=submissionDate:desc&page=1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?sort=submissionDate:desc&page=1"`
|
||||
);
|
||||
});
|
||||
|
||||
it('shows github stars and weekly downloads count for each plugin', async () => {
|
||||
@ -417,16 +419,22 @@ describe('Marketplace page - plugins tab', () => {
|
||||
// Can go to next page
|
||||
await user.click(getByText(/go to next page/i).closest('a')!);
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?page=2')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?pageSize=24&page=2"`
|
||||
);
|
||||
|
||||
// Can go to previous page
|
||||
await user.click(getByText(/go to previous page/i).closest('a')!);
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?page=1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?pageSize=24&page=1"`
|
||||
);
|
||||
|
||||
// Can go to specific page
|
||||
await user.click(getByText(/go to page 3/i).closest('a')!);
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?page=3')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?pageSize=24&page=3"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -30,7 +30,7 @@ jest.mock('@strapi/helper-plugin', () => ({
|
||||
const LocationDisplay = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return <span>{location.search}</span>;
|
||||
return <span data-testId="location">{location.search}</span>;
|
||||
};
|
||||
|
||||
const render = () =>
|
||||
@ -221,7 +221,7 @@ describe('Marketplace page - providers tab', () => {
|
||||
});
|
||||
|
||||
it('removes a filter option tag', async () => {
|
||||
const { getByRole, user, getByText } = render();
|
||||
const { getByRole, user } = render();
|
||||
|
||||
await waitForReload();
|
||||
|
||||
@ -240,7 +240,9 @@ describe('Marketplace page - providers tab', () => {
|
||||
await user.click(getByRole('button', { name: 'Made by Strapi' }));
|
||||
|
||||
await waitForReload();
|
||||
expect(getByText('?npmPackageType=provider&sort=name:asc&page=1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?npmPackageType=provider&sort=name:asc&page=1"`
|
||||
);
|
||||
});
|
||||
|
||||
it('only filters in the providers tab', async () => {
|
||||
@ -296,9 +298,9 @@ describe('Marketplace page - providers tab', () => {
|
||||
|
||||
await waitForReload();
|
||||
|
||||
expect(
|
||||
screen.getByText('?npmPackageType=provider&sort=submissionDate:desc&page=1')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?npmPackageType=provider&sort=submissionDate:desc&page=1"`
|
||||
);
|
||||
});
|
||||
|
||||
it('shows github stars and weekly downloads count for each provider', async () => {
|
||||
@ -340,16 +342,22 @@ describe('Marketplace page - providers tab', () => {
|
||||
// Can go to next page
|
||||
await user.click(getByText(/go to next page/i).closest('a')!);
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?npmPackageType=provider&sort=name:asc&page=2')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?pageSize=24&page=2&npmPackageType=provider&sort=name%3Aasc"`
|
||||
);
|
||||
|
||||
// Can go to previous page
|
||||
await user.click(getByText(/go to previous page/i).closest('a')!);
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?npmPackageType=provider&sort=name:asc&page=1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?pageSize=24&page=1&npmPackageType=provider&sort=name%3Aasc"`
|
||||
);
|
||||
|
||||
// Can go to specific page
|
||||
await user.click(getByText(/go to page 3/i).closest('a')!);
|
||||
await waitForReload();
|
||||
expect(screen.getByText('?npmPackageType=provider&sort=name:asc&page=3')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('location').textContent).toMatchInlineSnapshot(
|
||||
`"?pageSize=24&page=3&npmPackageType=provider&sort=name%3Aasc"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
DynamicTable,
|
||||
onRowClick,
|
||||
pxToRem,
|
||||
RelativeTime,
|
||||
useQueryParams,
|
||||
useTracking,
|
||||
TableProps as DynamicTableProps,
|
||||
@ -20,6 +19,7 @@ import styled from 'styled-components';
|
||||
|
||||
import { ApiToken } from '../../../../../../shared/contracts/api-token';
|
||||
import { SanitizedTransferToken } from '../../../../../../shared/contracts/transfer';
|
||||
import { RelativeTime } from '../../../../components/RelativeTime';
|
||||
|
||||
import type { Entity } from '@strapi/types';
|
||||
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
import {
|
||||
ConfirmDialog,
|
||||
getFetchClient,
|
||||
SearchURLQuery,
|
||||
useAPIErrorHandler,
|
||||
useFocusWhenNavigate,
|
||||
useQueryParams,
|
||||
@ -33,6 +32,7 @@ import { useIntl } from 'react-intl';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { Page } from '../../../../components/PageHelpers';
|
||||
import { SearchInput } from '../../../../components/SearchInput';
|
||||
import { useTypedSelector } from '../../../../core/store/hooks';
|
||||
import { useAdminRoles, AdminRole } from '../../../../hooks/useAdminRoles';
|
||||
import { selectAdminPermissions } from '../../../../selectors';
|
||||
@ -165,7 +165,7 @@ const ListPage = () => {
|
||||
{canRead && (
|
||||
<ActionLayout
|
||||
startActions={
|
||||
<SearchURLQuery
|
||||
<SearchInput
|
||||
label={formatMessage(
|
||||
{ id: 'app.component.search.label', defaultMessage: 'Search for {target}' },
|
||||
{
|
||||
|
||||
@ -7,18 +7,14 @@ import {
|
||||
Main,
|
||||
Flex,
|
||||
Typography,
|
||||
Box,
|
||||
Status,
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
DynamicTable,
|
||||
SearchURLQuery,
|
||||
useAPIErrorHandler,
|
||||
useFocusWhenNavigate,
|
||||
useNotification,
|
||||
useRBAC,
|
||||
PageSizeURLQuery,
|
||||
PaginationURLQuery,
|
||||
TableHeader,
|
||||
} from '@strapi/helper-plugin';
|
||||
import * as qs from 'qs';
|
||||
@ -28,6 +24,8 @@ import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { SanitizedAdminUser } from '../../../../../../shared/contracts/shared';
|
||||
import { Page } from '../../../../components/PageHelpers';
|
||||
import { Pagination } from '../../../../components/Pagination';
|
||||
import { SearchInput } from '../../../../components/SearchInput';
|
||||
import { useTypedSelector } from '../../../../core/store/hooks';
|
||||
import { useEnterprise } from '../../../../hooks/useEnterprise';
|
||||
import { useAdminUsers, useDeleteManyUsersMutation } from '../../../../services/users';
|
||||
@ -111,7 +109,7 @@ const ListPageCE = () => {
|
||||
<ActionLayout
|
||||
startActions={
|
||||
<>
|
||||
<SearchURLQuery
|
||||
<SearchInput
|
||||
label={formatMessage(
|
||||
{ id: 'app.component.search.label', defaultMessage: 'Search for {target}' },
|
||||
{ target: title }
|
||||
@ -157,15 +155,10 @@ const ListPageCE = () => {
|
||||
>
|
||||
<TableRows canDelete={canDelete} />
|
||||
</DynamicTable>
|
||||
|
||||
{pagination && (
|
||||
<Box paddingTop={4}>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
<Pagination.Root {...pagination}>
|
||||
<Pagination.PageSize />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
</>
|
||||
)}
|
||||
</ContentLayout>
|
||||
|
||||
@ -1,22 +1,15 @@
|
||||
import {
|
||||
ActionLayout,
|
||||
Box,
|
||||
ContentLayout,
|
||||
HeaderLayout,
|
||||
Layout,
|
||||
Main,
|
||||
} from '@strapi/design-system';
|
||||
import { ActionLayout, ContentLayout, HeaderLayout, Main } from '@strapi/design-system';
|
||||
import { DynamicTable, useFocusWhenNavigate, useQueryParams, useRBAC } from '@strapi/helper-plugin';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { Page } from '../../../../../../../admin/src/components/PageHelpers';
|
||||
import { Pagination } from '../../../../../../../admin/src/components/Pagination';
|
||||
import { useTypedSelector } from '../../../../../../../admin/src/core/store/hooks';
|
||||
import { Filters } from '../../../../../../../admin/src/pages/Settings/components/Filters';
|
||||
import { SanitizedAdminUserForAuditLogs } from '../../../../../../../shared/contracts/audit-logs';
|
||||
|
||||
import { Modal } from './components/Modal';
|
||||
import { PaginationFooter } from './components/PaginationFooter';
|
||||
import { TableHeader, TableRows } from './components/TableRows';
|
||||
import { useAuditLogsData } from './hooks/useAuditLogsData';
|
||||
import { getDisplayedFilters } from './utils/getDisplayedFilters';
|
||||
@ -130,7 +123,10 @@ const ListPage = () => {
|
||||
onOpenModal={(id) => setQuery({ id: `${id}` })}
|
||||
/>
|
||||
</DynamicTable>
|
||||
{auditLogs?.pagination && <PaginationFooter pagination={auditLogs.pagination} />}
|
||||
<Pagination.Root {...auditLogs?.pagination} defaultPageSize={24}>
|
||||
<Pagination.PageSize options={['12', '24', '50', '100']} />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
</ContentLayout>
|
||||
{query?.id && <Modal handleClose={() => setQuery({ id: null }, 'remove')} logId={query.id} />}
|
||||
</Main>
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Flex } from '@strapi/design-system';
|
||||
import { PageSizeURLQuery, PaginationURLQuery } from '@strapi/helper-plugin';
|
||||
|
||||
import { Pagination } from '../../../../../../../../shared/contracts/shared';
|
||||
|
||||
type PaginationFooterProps = {
|
||||
pagination: Pagination;
|
||||
};
|
||||
|
||||
export const PaginationFooter = (
|
||||
{ pagination }: PaginationFooterProps = {
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageCount: 0,
|
||||
pageSize: 50,
|
||||
total: 0,
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
<Box paddingTop={4}>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,6 @@
|
||||
import { within } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@tests/utils';
|
||||
import { render, screen, server, waitFor } from '@tests/utils';
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { ListPage } from '../ListPage';
|
||||
|
||||
@ -44,6 +45,28 @@ describe('ADMIN | Pages | AUDIT LOGS | ListPage', () => {
|
||||
});
|
||||
|
||||
expect(screen.getAllByRole('gridcell', { name: 'test testing' })).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should have pagination when theres enough data', async () => {
|
||||
server.use(
|
||||
rest.get('/admin/audit-logs', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.json({
|
||||
results: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageCount: 5,
|
||||
total: 50,
|
||||
},
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
render(<ListPage />);
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('Loading content...')).not.toBeInTheDocument());
|
||||
|
||||
expect(screen.getByRole('combobox', { name: 'Entries per page' })).toBeInTheDocument();
|
||||
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Duration, intervalToDuration, isPast } from 'date-fns';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const intervals: Array<keyof Duration> = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
|
||||
|
||||
interface CustomInterval {
|
||||
unit: keyof Duration;
|
||||
text: string;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
interface RelativeTimeProps extends React.ComponentPropsWithoutRef<'time'> {
|
||||
timestamp: Date;
|
||||
customIntervals?: CustomInterval[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the relative time between a given timestamp and the current time.
|
||||
* You can display a custom message for given time intervals by passing an array of custom intervals.
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <caption>Display "last hour" if the timestamp is less than an hour ago</caption>
|
||||
* <RelativeTime
|
||||
* timestamp={new Date('2021-01-01')}
|
||||
* customIntervals={[
|
||||
* { unit: 'hours', threshold: 1, text: 'last hour' },
|
||||
* ]}
|
||||
* ```
|
||||
*/
|
||||
const RelativeTime = React.forwardRef<HTMLTimeElement, RelativeTimeProps>(
|
||||
({ timestamp, customIntervals = [], ...restProps }, forwardedRef) => {
|
||||
const { formatRelativeTime, formatDate, formatTime } = useIntl();
|
||||
|
||||
/**
|
||||
* TODO: make this auto-update, like a clock.
|
||||
*/
|
||||
const interval = intervalToDuration({
|
||||
start: timestamp,
|
||||
end: Date.now(),
|
||||
// see https://github.com/date-fns/date-fns/issues/2891 – No idea why it's all partial it returns it every time.
|
||||
}) as Required<Duration>;
|
||||
|
||||
const unit = intervals.find((intervalUnit) => {
|
||||
return interval[intervalUnit] > 0 && Object.keys(interval).includes(intervalUnit);
|
||||
})!;
|
||||
|
||||
const relativeTime = isPast(timestamp) ? -interval[unit] : interval[unit];
|
||||
|
||||
// Display custom text if interval is less than the threshold
|
||||
const customInterval = customIntervals.find(
|
||||
(custom) => interval[custom.unit] < custom.threshold
|
||||
);
|
||||
|
||||
const displayText = customInterval
|
||||
? customInterval.text
|
||||
: formatRelativeTime(relativeTime, unit, { numeric: 'auto' });
|
||||
|
||||
return (
|
||||
<time
|
||||
ref={forwardedRef}
|
||||
dateTime={timestamp.toISOString()}
|
||||
role="time"
|
||||
title={`${formatDate(timestamp)} ${formatTime(timestamp)}`}
|
||||
{...restProps}
|
||||
>
|
||||
{displayText}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { RelativeTime };
|
||||
export type { CustomInterval, RelativeTimeProps };
|
||||
@ -0,0 +1,47 @@
|
||||
import { render, screen } from '@tests/utils';
|
||||
|
||||
import { RelativeTime } from '../RelativeTime';
|
||||
|
||||
const setDateNow = (now: number): jest.Spied<typeof Date.now> =>
|
||||
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||
|
||||
let spiedDateNow: ReturnType<typeof setDateNow> | undefined = undefined;
|
||||
|
||||
describe('RelativeTime', () => {
|
||||
afterAll(() => {
|
||||
if (spiedDateNow) {
|
||||
spiedDateNow.mockReset();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders and matches the snapshot', () => {
|
||||
spiedDateNow = setDateNow(1443686400000); // 2015-10-01 08:00:00
|
||||
render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(screen.getByRole('time')).toMatchInlineSnapshot(`
|
||||
<time
|
||||
datetime="2015-10-01T07:55:00.000Z"
|
||||
role="time"
|
||||
title="10/1/2015 7:55 AM"
|
||||
>
|
||||
5 minutes ago
|
||||
</time>
|
||||
`);
|
||||
});
|
||||
|
||||
it('can display the relative time for a future date', () => {
|
||||
spiedDateNow = setDateNow(1443685800000); // 2015-10-01 07:50:00
|
||||
render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(screen.getByRole('time')).toHaveTextContent('in 5 minutes');
|
||||
// expect(getByText('in 5 minutes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can display the relative time for a past date', () => {
|
||||
spiedDateNow = setDateNow(1443686400000); // 2015-10-01 08:00:00
|
||||
render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(screen.getByRole('time')).toHaveTextContent('5 minutes ago');
|
||||
// expect(getByText('5 minutes ago')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Page, unstable_useDocument } from '@strapi/admin/strapi-admin';
|
||||
import { Page, unstable_useDocument, Pagination } from '@strapi/admin/strapi-admin';
|
||||
import {
|
||||
Button,
|
||||
ContentLayout,
|
||||
@ -22,9 +22,6 @@ import {
|
||||
import { LinkButton, Menu } from '@strapi/design-system/v2';
|
||||
import {
|
||||
CheckPermissions,
|
||||
PageSizeURLQuery,
|
||||
PaginationURLQuery,
|
||||
RelativeTime,
|
||||
Table,
|
||||
useAPIErrorHandler,
|
||||
useNotification,
|
||||
@ -48,6 +45,7 @@ import { useIntl } from 'react-intl';
|
||||
import { useParams, useNavigate, Link as ReactRouterLink, Navigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { RelativeTime } from '../components/RelativeTime';
|
||||
import { ReleaseActionMenu } from '../components/ReleaseActionMenu';
|
||||
import { ReleaseActionOptions } from '../components/ReleaseActionOptions';
|
||||
import { ReleaseModal, FormValues } from '../components/ReleaseModal';
|
||||
@ -819,14 +817,13 @@ const ReleaseDetailsBody = ({ releaseId }: ReleaseDetailsBodyProps) => {
|
||||
</Table.Root>
|
||||
</Flex>
|
||||
))}
|
||||
<Flex paddingTop={4} alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery defaultValue={releaseMeta?.pagination?.pageSize.toString()} />
|
||||
<PaginationURLQuery
|
||||
pagination={{
|
||||
pageCount: releaseMeta?.pagination?.pageCount || 0,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Pagination.Root
|
||||
{...releaseMeta?.pagination}
|
||||
defaultPageSize={releaseMeta?.pagination?.pageSize}
|
||||
>
|
||||
<Pagination.PageSize />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
</Flex>
|
||||
</ContentLayout>
|
||||
);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// TODO: Replace this import with the same hook exported from the @strapi/admin/strapi-admin/ee in another iteration of this solution
|
||||
import { Page, useLicenseLimits } from '@strapi/admin/strapi-admin';
|
||||
import { Page, Pagination, useLicenseLimits } from '@strapi/admin/strapi-admin';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@ -25,13 +25,10 @@ import {
|
||||
import { Link } from '@strapi/design-system/v2';
|
||||
import {
|
||||
CheckPermissions,
|
||||
PageSizeURLQuery,
|
||||
PaginationURLQuery,
|
||||
useQueryParams,
|
||||
useAPIErrorHandler,
|
||||
useNotification,
|
||||
useTracking,
|
||||
RelativeTime,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { EmptyDocuments, Plus } from '@strapi/icons';
|
||||
import { useIntl } from 'react-intl';
|
||||
@ -39,6 +36,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { GetReleases, type Release } from '../../../shared/contracts/releases';
|
||||
import { RelativeTime } from '../components/RelativeTime';
|
||||
import { ReleaseModal, FormValues } from '../components/ReleaseModal';
|
||||
import { PERMISSIONS } from '../constants';
|
||||
import { isAxiosError } from '../services/axios';
|
||||
@ -404,19 +402,13 @@ const ReleasesPage = () => {
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
{response.currentData?.meta?.pagination?.total ? (
|
||||
<Flex paddingTop={4} alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery
|
||||
options={['8', '16', '32', '64']}
|
||||
defaultValue={response?.currentData?.meta?.pagination?.pageSize.toString()}
|
||||
/>
|
||||
<PaginationURLQuery
|
||||
pagination={{
|
||||
pageCount: response?.currentData?.meta?.pagination?.pageCount || 0,
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
) : null}
|
||||
<Pagination.Root
|
||||
{...response?.currentData?.meta?.pagination}
|
||||
defaultPageSize={response?.currentData?.meta?.pagination?.pageSize}
|
||||
>
|
||||
<Pagination.PageSize options={['8', '16', '32', '64']} />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
</>
|
||||
</ContentLayout>
|
||||
{releaseModalShown && (
|
||||
|
||||
@ -236,10 +236,70 @@ const MyComponent = (props) => {
|
||||
};
|
||||
```
|
||||
|
||||
### PageSizeURLQuery
|
||||
|
||||
This component was moved to the `admin` package and can now be imported via the `@strapi/strapi` package as part of the composite component `Pagination`:
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
import { PageSizeURLQuery } from '@strapi/helper-plugin';
|
||||
|
||||
// After
|
||||
import { Pagination } from '@strapi/strapi/admin';
|
||||
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<Pagination.Root>
|
||||
<Pagination.PageSize />
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Note, there were some slightly behavioural changes i.e. the PageSize won't render if the lowest pageSize is 10 but you only have 9 entries. Due to the refactor some props will have moved and changed, please look at the documentation for the Pagination component for more info.
|
||||
|
||||
### PaginationURLQueryProps
|
||||
|
||||
This component was moved to the `admin` package and can now be imported via the `@strapi/strapi` package as part of the composite component `Pagination`:
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
import { PaginationURLQueryProps } from '@strapi/helper-plugin';
|
||||
|
||||
// After
|
||||
import { Pagination } from '@strapi/strapi/admin';
|
||||
|
||||
const MyComponent = () => {
|
||||
return (
|
||||
<Pagination.Root pageCount={2}>
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Note, there were some slightly behavioural changes i.e. the Links won't render if there are less than 2 pages. Due to the refactor some props will have moved and changed, please look at the documentation for the Pagination component for more info.
|
||||
|
||||
### ReactSelect
|
||||
|
||||
This component has been removed and not replaced. If you feel like you need this component, please open an issue on the Strapi repository to discuss your usecase.
|
||||
|
||||
### RelativeTime
|
||||
|
||||
This component has been removed and not replaced. If you feel like you need this component, please open an issue on the Strapi repository to discuss your usecase.
|
||||
|
||||
### SearchURLQuery
|
||||
|
||||
This component was removed and renamed to `SearchInput` and can now be imported by the `@strapi/strapi` package:
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
import { SearchURLQuery } from '@strapi/helper-plugin';
|
||||
|
||||
// After
|
||||
import { SearchInput } from '@strapi/strapi/admin';
|
||||
```
|
||||
|
||||
### SettingsPageTitle
|
||||
|
||||
This component has been removed and not replaced. If you feel like you need this component, please open an issue on the Strapi repository to discuss your usecase.
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Flex, Loader } from '@strapi/design-system';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled(Flex)`
|
||||
height: 100vh;
|
||||
`;
|
||||
|
||||
interface LoadingIndicatorPageProps {
|
||||
children?: React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
const LoadingIndicatorPage = ({
|
||||
children = 'Loading content.',
|
||||
'data-testid': dataTestId = 'loader',
|
||||
}: LoadingIndicatorPageProps) => {
|
||||
return (
|
||||
<Wrapper justifyContent="space-around" data-testid={dataTestId}>
|
||||
<Loader>{children}</Loader>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export { LoadingIndicatorPage };
|
||||
@ -1,70 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* PageSizeURLQuery
|
||||
*
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { Flex, SingleSelectOption, SingleSelect, Typography } from '@strapi/design-system';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useTracking, TrackingEvent } from '../features/Tracking';
|
||||
import { useQueryParams } from '../hooks/useQueryParams';
|
||||
|
||||
export interface PageSizeURLQueryProps {
|
||||
trackedEvent?: Extract<TrackingEvent, { properties?: never }>['name'];
|
||||
options?: string[];
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export const PageSizeURLQuery = ({
|
||||
trackedEvent,
|
||||
options = ['10', '20', '50', '100'],
|
||||
defaultValue = '10',
|
||||
}: PageSizeURLQueryProps) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const [{ query }, setQuery] = useQueryParams<{ pageSize?: string; page: number }>();
|
||||
const { trackUsage } = useTracking();
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
if (trackedEvent) {
|
||||
trackUsage(trackedEvent);
|
||||
}
|
||||
|
||||
setQuery({
|
||||
pageSize: value,
|
||||
page: 1,
|
||||
});
|
||||
};
|
||||
|
||||
const pageSize: string =
|
||||
typeof query?.pageSize === 'string' && query?.pageSize !== '' ? query.pageSize : defaultValue;
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<SingleSelect
|
||||
size="S"
|
||||
aria-label={formatMessage({
|
||||
id: 'components.PageFooter.select',
|
||||
defaultMessage: 'Entries per page',
|
||||
})}
|
||||
// @ts-expect-error from the DS V2 this won't be needed because we're only returning strings.
|
||||
onChange={handleChange}
|
||||
value={pageSize}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<SingleSelectOption key={option} value={option}>
|
||||
{option}
|
||||
</SingleSelectOption>
|
||||
))}
|
||||
</SingleSelect>
|
||||
<Typography textColor="neutral600" as="span">
|
||||
{formatMessage({
|
||||
id: 'components.PageFooter.select',
|
||||
defaultMessage: 'Entries per page',
|
||||
})}
|
||||
</Typography>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@ -1,146 +0,0 @@
|
||||
/**
|
||||
* Pagination
|
||||
*
|
||||
* The component works as follows
|
||||
* `1` , 2, 3, ... 10
|
||||
* 1, `2`, 3, ... 10
|
||||
* 1, 2, `3`, 4, ... 10
|
||||
* 1, 2, 3, `4`, 5, ... 10
|
||||
* 1, ..,4, `5`, 6, ... 10
|
||||
*
|
||||
* 1, ...., 8, 9, `10`
|
||||
* 1, ...., 8, `9`, 10
|
||||
* 1, ...., 7, `8`, 9, 10
|
||||
* 1, ... 6, `7`, 8, 9, 10
|
||||
*/
|
||||
|
||||
import { Dots, NextLink, PageLink, Pagination, PreviousLink } from '@strapi/design-system';
|
||||
import { stringify } from 'qs';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useQueryParams } from '../hooks/useQueryParams';
|
||||
|
||||
type PaginationURLQueryProps = {
|
||||
/**
|
||||
* Number of always visible pages at the beginning and end.
|
||||
* @default 1
|
||||
*/
|
||||
boundaryCount?: number;
|
||||
pagination: {
|
||||
pageCount: number;
|
||||
};
|
||||
/**
|
||||
* Number of always visible pages before and after the current page.
|
||||
* @default 1
|
||||
*/
|
||||
siblingCount?: number;
|
||||
};
|
||||
|
||||
export const PaginationURLQuery = ({
|
||||
pagination: { pageCount },
|
||||
boundaryCount = 1,
|
||||
siblingCount = 1,
|
||||
}: PaginationURLQueryProps) => {
|
||||
const [{ query }] = useQueryParams<{ page: string }>();
|
||||
const activePage = parseInt(query?.page || '1', 10);
|
||||
const { pathname } = useLocation();
|
||||
const { formatMessage } = useIntl();
|
||||
const makeSearch = (page: number) => stringify({ ...query, page }, { encode: false });
|
||||
|
||||
const nextSearch = makeSearch(activePage + (pageCount > 1 ? 1 : 0));
|
||||
|
||||
const previousSearch = makeSearch(activePage - 1);
|
||||
|
||||
const range = (start: number, end: number) => {
|
||||
const length = end - start + 1;
|
||||
|
||||
return Array.from({ length }, (_, i) => start + i);
|
||||
};
|
||||
|
||||
const startPages = range(1, Math.min(boundaryCount, pageCount));
|
||||
const endPages = range(Math.max(pageCount - boundaryCount + 1, boundaryCount + 1), pageCount);
|
||||
|
||||
const siblingsStart = Math.max(
|
||||
Math.min(
|
||||
// Natural start
|
||||
activePage - siblingCount,
|
||||
// Lower boundary when page is high
|
||||
pageCount - boundaryCount - siblingCount * 2 - 1
|
||||
),
|
||||
// Greater than startPages
|
||||
boundaryCount + 2
|
||||
);
|
||||
|
||||
const siblingsEnd = Math.min(
|
||||
Math.max(
|
||||
// Natural end
|
||||
activePage + siblingCount,
|
||||
// Upper boundary when page is low
|
||||
boundaryCount + siblingCount * 2 + 2
|
||||
),
|
||||
// Less than endPages
|
||||
endPages.length > 0 ? endPages[0] - 2 : pageCount - 1
|
||||
);
|
||||
|
||||
const items = [
|
||||
...startPages,
|
||||
|
||||
// Start ellipsis
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
...(siblingsStart > boundaryCount + 2
|
||||
? ['start-ellipsis']
|
||||
: boundaryCount + 1 < pageCount - boundaryCount
|
||||
? [boundaryCount + 1]
|
||||
: []),
|
||||
|
||||
// Sibling pages
|
||||
...range(siblingsStart, siblingsEnd),
|
||||
|
||||
// End ellipsis
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
...(siblingsEnd < pageCount - boundaryCount - 1
|
||||
? ['end-ellipsis']
|
||||
: pageCount - boundaryCount > boundaryCount
|
||||
? [pageCount - boundaryCount]
|
||||
: []),
|
||||
|
||||
...endPages,
|
||||
];
|
||||
|
||||
return (
|
||||
<Pagination activePage={activePage} pageCount={pageCount}>
|
||||
<PreviousLink active={false} to={{ pathname, search: previousSearch }}>
|
||||
{formatMessage({
|
||||
id: 'components.pagination.go-to-previous',
|
||||
defaultMessage: 'Go to previous page',
|
||||
})}
|
||||
</PreviousLink>
|
||||
{items.map((item) => {
|
||||
if (typeof item === 'number') {
|
||||
return (
|
||||
<PageLink
|
||||
active={item === activePage}
|
||||
key={item}
|
||||
number={item}
|
||||
to={{ pathname, search: makeSearch(item) }}
|
||||
>
|
||||
{formatMessage(
|
||||
{ id: 'components.pagination.go-to', defaultMessage: 'Go to page {page}' },
|
||||
{ page: item }
|
||||
)}
|
||||
</PageLink>
|
||||
);
|
||||
}
|
||||
|
||||
return <Dots key={item} />;
|
||||
})}
|
||||
<NextLink active={false} to={{ pathname, search: nextSearch }}>
|
||||
{formatMessage({
|
||||
id: 'components.pagination.go-to-next',
|
||||
defaultMessage: 'Go to next page',
|
||||
})}
|
||||
</NextLink>
|
||||
</Pagination>
|
||||
);
|
||||
};
|
||||
@ -1,65 +0,0 @@
|
||||
import { Duration, intervalToDuration, isPast } from 'date-fns';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
const intervals: Array<keyof Duration> = ['years', 'months', 'days', 'hours', 'minutes', 'seconds'];
|
||||
|
||||
interface CustomInterval {
|
||||
unit: keyof Duration;
|
||||
text: string;
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export interface RelativeTimeProps {
|
||||
timestamp: Date;
|
||||
customIntervals?: CustomInterval[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the relative time between a given timestamp and the current time.
|
||||
* You can display a custom message for given time intervals by passing an array of custom intervals.
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <caption>Display "last hour" if the timestamp is less than an hour ago</caption>
|
||||
* <RelativeTime
|
||||
* timestamp={new Date('2021-01-01')}
|
||||
* customIntervals={[
|
||||
* { unit: 'hours', threshold: 1, text: 'last hour' },
|
||||
* ]}
|
||||
* ```
|
||||
*/
|
||||
const RelativeTime = ({ timestamp, customIntervals = [], className }: RelativeTimeProps) => {
|
||||
const { formatRelativeTime, formatDate, formatTime } = useIntl();
|
||||
|
||||
const interval = intervalToDuration({
|
||||
start: timestamp,
|
||||
end: Date.now(),
|
||||
// see https://github.com/date-fns/date-fns/issues/2891 – No idea why it's all partial it returns it every time.
|
||||
}) as Required<Duration>;
|
||||
|
||||
const unit = intervals.find((intervalUnit) => {
|
||||
return interval[intervalUnit] > 0 && Object.keys(interval).includes(intervalUnit);
|
||||
})!;
|
||||
|
||||
const relativeTime = isPast(timestamp) ? -interval[unit] : interval[unit];
|
||||
|
||||
// Display custom text if interval is less than the threshold
|
||||
const customInterval = customIntervals.find((custom) => interval[custom.unit] < custom.threshold);
|
||||
|
||||
const displayText = customInterval
|
||||
? customInterval.text
|
||||
: formatRelativeTime(relativeTime, unit, { numeric: 'auto' });
|
||||
|
||||
return (
|
||||
<time
|
||||
dateTime={timestamp.toISOString()}
|
||||
title={`${formatDate(timestamp)} ${formatTime(timestamp)}`}
|
||||
className={className}
|
||||
>
|
||||
{displayText}
|
||||
</time>
|
||||
);
|
||||
};
|
||||
|
||||
export { RelativeTime };
|
||||
@ -1,121 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* Tests for PageSizeURLQuery
|
||||
*
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { render } from '@tests/utils';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { PageSizeURLQuery } from '../PageSizeURLQuery';
|
||||
|
||||
const trackUsage = jest.fn();
|
||||
|
||||
jest.mock('../../features/Tracking', () => ({
|
||||
useTracking: () => ({
|
||||
trackUsage,
|
||||
}),
|
||||
}));
|
||||
|
||||
const LocationDisplay = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<ul>
|
||||
<li>{location.search}</li>
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
describe('PageSizeURLQuery', () => {
|
||||
it('renders', async () => {
|
||||
const { getByRole, getAllByRole, getByText, user } = render(<PageSizeURLQuery />);
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('10');
|
||||
expect(getByText('Entries per page')).toBeInTheDocument();
|
||||
|
||||
await user.click(getByRole('combobox'));
|
||||
|
||||
expect(getAllByRole('option')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('should display the default pageSize correctly if passed as a prop', () => {
|
||||
const { getByRole } = render(<PageSizeURLQuery defaultValue="20" />);
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('20');
|
||||
});
|
||||
|
||||
it('should display the pageSize correctly if its in the url query', () => {
|
||||
const { getByRole } = render(<PageSizeURLQuery />, {
|
||||
initialEntries: [{ search: 'pageSize=50' }],
|
||||
});
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('50');
|
||||
});
|
||||
|
||||
it('should render a custom list of options if provided', async () => {
|
||||
const options = ['5', '10', '15'];
|
||||
|
||||
const { getByRole, user } = render(<PageSizeURLQuery options={options} />);
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('10');
|
||||
|
||||
await user.click(getByRole('combobox'));
|
||||
|
||||
options.forEach((option) => {
|
||||
expect(getByRole('option', { name: option })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interaction', () => {
|
||||
it('should change the value when the user selects a new value', async () => {
|
||||
const { getByRole, user } = render(<PageSizeURLQuery />, {
|
||||
renderOptions: {
|
||||
wrapper({ children }) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<LocationDisplay />
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await user.click(getByRole('combobox'));
|
||||
|
||||
await user.click(getByRole('option', { name: '20' }));
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('20');
|
||||
|
||||
const searchParams = new URLSearchParams(getByRole('listitem').textContent ?? '');
|
||||
|
||||
expect(searchParams.has('pageSize')).toBe(true);
|
||||
expect(searchParams.get('pageSize')).toBe('20');
|
||||
});
|
||||
|
||||
it('should use the default value and then change the value when the user selects a new value', async () => {
|
||||
const { getByRole, user } = render(<PageSizeURLQuery defaultValue="20" />);
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('20');
|
||||
|
||||
await user.click(getByRole('combobox'));
|
||||
|
||||
await user.click(getByRole('option', { name: '50' }));
|
||||
|
||||
expect(getByRole('combobox')).toHaveTextContent('50');
|
||||
});
|
||||
|
||||
it('should call trackUsage with trackedEvent props when submit', async () => {
|
||||
const { getByRole, user } = render(<PageSizeURLQuery trackedEvent="didCreateRole" />);
|
||||
|
||||
await user.click(getByRole('combobox'));
|
||||
|
||||
await user.click(getByRole('option', { name: '50' }));
|
||||
|
||||
expect(trackUsage.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,161 +0,0 @@
|
||||
import { render } from '@tests/utils';
|
||||
|
||||
import { PaginationURLQuery } from '../PaginationURLQuery';
|
||||
|
||||
describe('PaginationURLQuery', () => {
|
||||
it('renders when there is only one page', () => {
|
||||
const { getByRole } = render(<PaginationURLQuery pagination={{ pageCount: 1 }} />);
|
||||
|
||||
expect(getByRole('navigation')).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toHaveAttribute('aria-current', 'page');
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('should display 4 links when the pageCount is greater than 4', () => {
|
||||
const { getByRole } = render(<PaginationURLQuery pagination={{ pageCount: 4 }} />);
|
||||
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toHaveAttribute('aria-current', 'page');
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 4' })).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render only 4 links when the page count is 5 or greater', () => {
|
||||
const { getByRole, rerender } = render(<PaginationURLQuery pagination={{ pageCount: 5 }} />);
|
||||
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'true'
|
||||
);
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toHaveAttribute('aria-current', 'page');
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 5' })).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
|
||||
rerender(<PaginationURLQuery pagination={{ pageCount: 10 }} />);
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 10' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render multiple dot elements when the page count is greater than 5 and the current page is greater than 4', () => {
|
||||
const { getByRole } = render(<PaginationURLQuery pagination={{ pageCount: 10 }} />, {
|
||||
initialEntries: [{ pathname: '/', search: 'page=5' }],
|
||||
});
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 1' })).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 4' })).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 5' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 5' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 6' })).toBeInTheDocument();
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 10' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set the default page to the same as the query', () => {
|
||||
const { getByRole } = render(<PaginationURLQuery pagination={{ pageCount: 4 }} />, {
|
||||
initialEntries: [{ pathname: '/', search: 'page=3' }],
|
||||
});
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to previous page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to next page' })).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
'false'
|
||||
);
|
||||
});
|
||||
|
||||
it('should change the page correctly when the user clicks on a page link', async () => {
|
||||
const { getByRole, user } = render(<PaginationURLQuery pagination={{ pageCount: 4 }} />, {
|
||||
initialEntries: [{ pathname: '/', search: 'page=3' }],
|
||||
});
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await user.click(getByRole('link', { name: 'Go to page 2' }));
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toHaveAttribute('aria-current', 'page');
|
||||
});
|
||||
|
||||
it('should change the page correctly when the user clicks on the arrows', async () => {
|
||||
const { getByRole, user } = render(<PaginationURLQuery pagination={{ pageCount: 4 }} />, {
|
||||
initialEntries: [{ pathname: '/', search: 'page=3' }],
|
||||
});
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await user.click(getByRole('link', { name: 'Go to previous page' }));
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await user.click(getByRole('link', { name: 'Go to next page' }));
|
||||
await user.click(getByRole('link', { name: 'Go to next page' }));
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 4' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 4' })).toHaveAttribute('aria-current', 'page');
|
||||
});
|
||||
|
||||
it('should carry the rest of the query params when the user clicks on a page link', async () => {
|
||||
const { getByRole, user } = render(<PaginationURLQuery pagination={{ pageCount: 4 }} />, {
|
||||
initialEntries: [{ pathname: '/', search: 'page=3&search=foo' }],
|
||||
});
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 3' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
await user.click(getByRole('link', { name: 'Go to page 2' }));
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toBeInTheDocument();
|
||||
expect(getByRole('link', { name: 'Go to page 2' })).toHaveAttribute('aria-current', 'page');
|
||||
|
||||
expect(getByRole('link', { name: 'Go to page 4' })).toHaveAttribute(
|
||||
'href',
|
||||
'/?page=4&search=foo'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,44 +0,0 @@
|
||||
import { render } from '@tests/utils';
|
||||
|
||||
import { RelativeTime } from '../RelativeTime';
|
||||
|
||||
export function setDateNow(now: number): jest.Spied<typeof Date.now> {
|
||||
return jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||
}
|
||||
let spiedDateNow: jest.Spied<typeof Date.now> | undefined = undefined;
|
||||
|
||||
describe('RelativeTime', () => {
|
||||
afterAll(() => {
|
||||
spiedDateNow?.mockReset();
|
||||
});
|
||||
|
||||
it('renders and matches the snapshot', () => {
|
||||
spiedDateNow = setDateNow(1443686400000); // 2015-10-01 08:00:00
|
||||
const {
|
||||
container: { firstChild },
|
||||
} = render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(firstChild).toMatchInlineSnapshot(`
|
||||
<time
|
||||
datetime="2015-10-01T07:55:00.000Z"
|
||||
title="10/1/2015 7:55 AM"
|
||||
>
|
||||
5 minutes ago
|
||||
</time>
|
||||
`);
|
||||
});
|
||||
|
||||
it('can display the relative time for a future date', () => {
|
||||
spiedDateNow = setDateNow(1443685800000); // 2015-10-01 07:50:00
|
||||
const { getByText } = render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(getByText('in 5 minutes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can display the relative time for a past date', () => {
|
||||
spiedDateNow = setDateNow(1443686400000); // 2015-10-01 08:00:00
|
||||
const { getByText } = render(<RelativeTime timestamp={new Date('2015-10-01 07:55:00')} />);
|
||||
|
||||
expect(getByText('5 minutes ago')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -14,7 +14,7 @@ const useQueryParams = <TQuery extends object>(initialParams?: TQuery) => {
|
||||
return initialParams;
|
||||
}
|
||||
|
||||
return parse(searchQuery) as TQuery;
|
||||
return { ...initialParams, ...parse(searchQuery) } as TQuery;
|
||||
}, [search, initialParams]);
|
||||
|
||||
const setQuery = useCallback(
|
||||
|
||||
@ -9,10 +9,6 @@ export * from './components/Table';
|
||||
export * from './components/EmptyBodyTable';
|
||||
export * from './components/FilterListURLQuery';
|
||||
export * from './components/FilterPopoverURLQuery';
|
||||
export * from './components/PageSizeURLQuery';
|
||||
export * from './components/PaginationURLQuery';
|
||||
export * from './components/RelativeTime';
|
||||
export * from './components/SearchURLQuery';
|
||||
|
||||
/* -------------------------------------------------------------------------------------------------
|
||||
* Features
|
||||
|
||||
@ -22,7 +22,7 @@ const makeApp = (queryValue) => (
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
describe('<SearchURLQuery />', () => {
|
||||
describe('SearchAsset', () => {
|
||||
it('renders and matches the snapshot', () => {
|
||||
const { container } = render(makeApp(null));
|
||||
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Flex } from '@strapi/design-system';
|
||||
import { PageSizeURLQuery, PaginationURLQuery } from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const PaginationFooter = ({ pagination }) => {
|
||||
return (
|
||||
<Box paddingTop={6}>
|
||||
<Flex alignItems="flex-end" justifyContent="space-between">
|
||||
<PageSizeURLQuery />
|
||||
<PaginationURLQuery pagination={pagination} />
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
PaginationFooter.defaultProps = {
|
||||
pagination: {
|
||||
pageCount: 0,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
PaginationFooter.propTypes = {
|
||||
pagination: PropTypes.shape({
|
||||
page: PropTypes.number,
|
||||
pageCount: PropTypes.number,
|
||||
pageSize: PropTypes.number,
|
||||
total: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import { Page } from '@strapi/admin/strapi-admin';
|
||||
import { Page, SearchInput, Pagination } from '@strapi/admin/strapi-admin';
|
||||
import {
|
||||
ActionLayout,
|
||||
BaseCheckbox,
|
||||
@ -17,7 +17,6 @@ import {
|
||||
} from '@strapi/design-system';
|
||||
import {
|
||||
CheckPermissions,
|
||||
SearchURLQuery,
|
||||
useFocusWhenNavigate,
|
||||
usePersistentState,
|
||||
useQueryParams,
|
||||
@ -40,7 +39,6 @@ import {
|
||||
FolderCardCheckbox,
|
||||
} from '../../../components/FolderCard';
|
||||
import { FolderGridList } from '../../../components/FolderGridList';
|
||||
import { PaginationFooter } from '../../../components/PaginationFooter';
|
||||
import SortPicker from '../../../components/SortPicker';
|
||||
import { TableList } from '../../../components/TableList';
|
||||
import { UploadAssetDialog } from '../../../components/UploadAssetDialog/UploadAssetDialog';
|
||||
@ -298,7 +296,7 @@ export const MediaLibrary = () => {
|
||||
onClick={() => setView(isGridView ? viewOptions.LIST : viewOptions.GRID)}
|
||||
/>
|
||||
</ActionContainer>
|
||||
<SearchURLQuery
|
||||
<SearchInput
|
||||
label={formatMessage({
|
||||
id: getTrad('search.label'),
|
||||
defaultMessage: 'Search for an asset',
|
||||
@ -478,8 +476,10 @@ export const MediaLibrary = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{assetsData?.pagination && <PaginationFooter pagination={assetsData.pagination} />}
|
||||
<Pagination.Root {...assetsData.pagination}>
|
||||
<Pagination.PageSize />
|
||||
<Pagination.Links />
|
||||
</Pagination.Root>
|
||||
</ContentLayout>
|
||||
</Main>
|
||||
|
||||
|
||||
@ -21,7 +21,6 @@ import { LinkButton } from '@strapi/design-system/v2';
|
||||
import {
|
||||
CheckPermissions,
|
||||
ConfirmDialog,
|
||||
SearchURLQuery,
|
||||
useFocusWhenNavigate,
|
||||
useNotification,
|
||||
useQueryParams,
|
||||
@ -29,7 +28,7 @@ import {
|
||||
useTracking,
|
||||
} from '@strapi/helper-plugin';
|
||||
import { Plus } from '@strapi/icons';
|
||||
import { Page } from '@strapi/strapi/admin';
|
||||
import { Page, SearchInput } from '@strapi/strapi/admin';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
@ -172,7 +171,7 @@ export const RolesListPage = () => {
|
||||
|
||||
<ActionLayout
|
||||
startActions={
|
||||
<SearchURLQuery
|
||||
<SearchInput
|
||||
label={formatMessage({
|
||||
id: 'app.component.search.label',
|
||||
defaultMessage: 'Search',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user