💄(frontend) add dropdown option for DocGridItem
Implement dropdown menu with functionality to delete a document from the list
This commit is contained in:
committed by
Anthony LC
parent
5a46ab0055
commit
1d85eee78f
@@ -91,6 +91,98 @@ test.describe('Documents Grid mobile', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Document grid item options', () => {
|
||||
test('it deletes the document', async ({ page }) => {
|
||||
let docs: SmallDoc[] = [];
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
docs = result.results as SmallDoc[];
|
||||
|
||||
const button = page.getByTestId(`docs-grid-actions-button-${docs[0].id}`);
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
|
||||
const removeButton = page.getByTestId(
|
||||
`docs-grid-actions-remove-${docs[0].id}`,
|
||||
);
|
||||
await expect(removeButton).toBeVisible();
|
||||
await removeButton.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${docs[0].title}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
const refetchResponse = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
|
||||
const resultRefetch = await refetchResponse.json();
|
||||
expect(resultRefetch.count).toBe(result.count - 1);
|
||||
await expect(page.getByTestId('main-layout-loader')).toBeHidden();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
await expect(button).toBeHidden();
|
||||
});
|
||||
|
||||
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route('*/**/api/v1.0/documents/?page=1', async (route) => {
|
||||
await route.fulfill({
|
||||
json: {
|
||||
results: [
|
||||
{
|
||||
id: 'mocked-document-id',
|
||||
content: '',
|
||||
title: 'Mocked document',
|
||||
accesses: [],
|
||||
abilities: {
|
||||
destroy: false, // Means not owner
|
||||
link_configuration: false,
|
||||
versions_destroy: false,
|
||||
versions_list: true,
|
||||
versions_retrieve: true,
|
||||
accesses_manage: false, // Means not admin
|
||||
update: false,
|
||||
partial_update: false, // Means not editor
|
||||
retrieve: true,
|
||||
},
|
||||
link_reach: 'restricted',
|
||||
created_at: '2021-09-01T09:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.goto('/');
|
||||
|
||||
const button = page.getByTestId(
|
||||
`docs-grid-actions-button-mocked-document-id`,
|
||||
);
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
const removeButton = page.getByTestId(
|
||||
`docs-grid-actions-remove-mocked-document-id`,
|
||||
);
|
||||
await expect(removeButton).toBeVisible();
|
||||
await removeButton.isDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Documents Grid', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -163,35 +255,4 @@ test.describe('Documents Grid', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('it deletes the document', async ({ page }) => {
|
||||
let docs: SmallDoc[] = [];
|
||||
const response = await page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('documents/?page=1') &&
|
||||
response.status() === 200,
|
||||
);
|
||||
const result = await response.json();
|
||||
docs = result.results as SmallDoc[];
|
||||
|
||||
const button = page.getByTestId(`docs-grid-delete-button-${docs[0].id}`);
|
||||
|
||||
await expect(button).toBeVisible();
|
||||
await button.click();
|
||||
|
||||
await expect(
|
||||
page.locator('h2').getByText(`Deleting the document "${docs[0].title}"`),
|
||||
).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Confirm deletion',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByText('The document has been deleted.'),
|
||||
).toBeVisible();
|
||||
await expect(button).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { ComponentPropsWithRef, forwardRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxType } from './Box';
|
||||
|
||||
export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
||||
export type BoxButtonType = BoxType & {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* Styleless button that extends the Box component.
|
||||
@@ -18,7 +22,7 @@ export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
||||
* </BoxButton>
|
||||
* ```
|
||||
*/
|
||||
const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
||||
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||
({ $css, ...props }, ref) => {
|
||||
return (
|
||||
<Box
|
||||
@@ -28,14 +32,24 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
||||
$margin="none"
|
||||
$padding="none"
|
||||
$css={css`
|
||||
cursor: pointer;
|
||||
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-family: inherit;
|
||||
|
||||
color: ${props.disabled
|
||||
? 'var(--c--theme--colors--greyscale-400) !important'
|
||||
: 'inherit'};
|
||||
${$css || ''}
|
||||
`}
|
||||
{...props}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
props.onClick?.(event);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
|
||||
import { Button, DialogTrigger, Popover } from 'react-aria-components';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@@ -11,7 +6,7 @@ const StyledPopover = styled(Popover)`
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5rem;
|
||||
|
||||
border: 1px solid #dddddd;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
@@ -29,7 +24,7 @@ const StyledButton = styled(Button)`
|
||||
text-wrap: nowrap;
|
||||
`;
|
||||
|
||||
interface DropButtonProps {
|
||||
export interface DropButtonProps {
|
||||
button: ReactNode;
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
|
||||
112
src/frontend/apps/impress/src/components/DropdownMenu.tsx
Normal file
112
src/frontend/apps/impress/src/components/DropdownMenu.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, BoxProps, DropButton, Icon } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
icon?: string;
|
||||
label: string;
|
||||
testId?: string;
|
||||
callback?: () => void | Promise<unknown>;
|
||||
danger?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type DropdownMenuProps = {
|
||||
options: DropdownMenuOption[];
|
||||
showArrow?: boolean;
|
||||
arrowCss?: BoxProps['$css'];
|
||||
};
|
||||
|
||||
export const DropdownMenu = ({
|
||||
options,
|
||||
children,
|
||||
showArrow = false,
|
||||
arrowCss,
|
||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||
const theme = useCunninghamTheme();
|
||||
const spacings = theme.spacingsTokens();
|
||||
const colors = theme.colorsTokens();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onOpenChange = (isOpen: boolean) => {
|
||||
setIsOpen(isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropButton
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
button={
|
||||
showArrow ? (
|
||||
<Box>
|
||||
<div>{children}</div>
|
||||
<Icon
|
||||
$css={
|
||||
arrowCss ??
|
||||
css`
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
`
|
||||
}
|
||||
iconName={isOpen ? 'arrow_drop_up' : 'arrow_drop_down'}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
children
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
{options.map((option, index) => {
|
||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||
return (
|
||||
<BoxButton
|
||||
data-testid={option.testId}
|
||||
$direction="row"
|
||||
disabled={isDisabled}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onOpenChange?.(false);
|
||||
void option.callback?.();
|
||||
}}
|
||||
key={option.label}
|
||||
$align="center"
|
||||
$background={colors['greyscale-000']}
|
||||
$color={colors['primary-600']}
|
||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
||||
$width="100%"
|
||||
$gap={spacings['base']}
|
||||
$css={css`
|
||||
border: none;
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
font-weight: 500;
|
||||
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
|
||||
user-select: none;
|
||||
border-bottom: ${index !== options.length - 1
|
||||
? `1px solid var(--c--theme--colors--greyscale-200)`
|
||||
: 'none'};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.icon && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme={!isDisabled ? 'primary' : 'greyscale'}
|
||||
$variation={!isDisabled ? '600' : '400'}
|
||||
iconName={option.icon}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</BoxButton>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</DropButton>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ export * from './Box';
|
||||
export * from './BoxButton';
|
||||
export * from './Card';
|
||||
export * from './DropButton';
|
||||
export * from './DropdownMenu';
|
||||
export * from './Icon';
|
||||
export * from './InfiniteScroll';
|
||||
export * from './Link';
|
||||
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { t } from 'i18next';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham/';
|
||||
|
||||
import { useRemoveDoc } from '../api/useRemoveDoc';
|
||||
import IconDoc from '../assets/icon-doc.svg';
|
||||
import { Doc } from '../types';
|
||||
|
||||
interface ModalRemoveDocProps {
|
||||
@@ -22,13 +21,13 @@ interface ModalRemoveDocProps {
|
||||
}
|
||||
|
||||
export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { toast } = useToastProvider();
|
||||
const { push } = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const {
|
||||
mutate: removeDoc,
|
||||
|
||||
isError,
|
||||
error,
|
||||
} = useRemoveDoc({
|
||||
@@ -36,7 +35,11 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
void push('/');
|
||||
if (pathname === '/') {
|
||||
onClose();
|
||||
} else {
|
||||
void push('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -59,7 +62,7 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
rightActions={
|
||||
<Button
|
||||
aria-label={t('Confirm deletion')}
|
||||
color="primary"
|
||||
color="danger"
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
removeDoc({
|
||||
@@ -97,32 +100,6 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||
)}
|
||||
|
||||
{isError && <TextErrors causes={error.cause} />}
|
||||
|
||||
<Text
|
||||
as="p"
|
||||
$padding="small"
|
||||
$direction="row"
|
||||
$gap="0.5rem"
|
||||
$background={colorsTokens()['primary-150']}
|
||||
$theme="primary"
|
||||
$align="center"
|
||||
$radius="2px"
|
||||
>
|
||||
<IconDoc
|
||||
className="p-t"
|
||||
aria-label={t(`Document icon`)}
|
||||
color={colorsTokens()['primary-500']}
|
||||
width={58}
|
||||
style={{
|
||||
borderRadius: '8px',
|
||||
backgroundColor: '#ffffff',
|
||||
border: `1px solid ${colorsTokens()['primary-300']}`,
|
||||
}}
|
||||
/>
|
||||
<Text $theme="primary" $weight="bold" $size="l">
|
||||
{doc.title}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -8,16 +8,23 @@ import { useResponsiveStore } from '@/stores';
|
||||
import { useInfiniteDocs } from '../../doc-management';
|
||||
|
||||
import { DocsGridItem } from './DocsGridItem';
|
||||
import { DocsGridLoader } from './DocsGridLoader';
|
||||
|
||||
export const DocsGrid = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const { data, isFetching, isLoading, fetchNextPage, hasNextPage } =
|
||||
useInfiniteDocs({
|
||||
page: 1,
|
||||
});
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isRefetching,
|
||||
isLoading,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
} = useInfiniteDocs({
|
||||
page: 1,
|
||||
});
|
||||
const loading = isFetching || isLoading;
|
||||
|
||||
const loadMore = (inView: boolean) => {
|
||||
@@ -28,65 +35,69 @@ export const DocsGrid = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card data-testid="docs-grid" $padding="md" $width="100%" $maxWidth="960px">
|
||||
<Text
|
||||
as="h4"
|
||||
$size="h4"
|
||||
$weight="700"
|
||||
$margin={{ top: '0px', bottom: 'xs' }}
|
||||
>
|
||||
{t('All docs')}
|
||||
</Text>
|
||||
<Box $position="relative" $width="100%" $maxWidth="960px">
|
||||
<DocsGridLoader isLoading={isRefetching} />
|
||||
<Card data-testid="docs-grid" $padding="md">
|
||||
<Text
|
||||
as="h4"
|
||||
$size="h4"
|
||||
$weight="700"
|
||||
$margin={{ top: '0px', bottom: 'xs' }}
|
||||
>
|
||||
{t('All docs')}
|
||||
</Text>
|
||||
|
||||
<Box>
|
||||
<Box $direction="row" $padding="xs" data-testid="docs-grid-header">
|
||||
<Box $flex={6} $padding="3xs">
|
||||
<Text $size="xs" $variation="600">
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
{isDesktop && (
|
||||
<Box $flex={1} $padding="3xs">
|
||||
<Box>
|
||||
<Box $direction="row" $padding="xs" data-testid="docs-grid-header">
|
||||
<Box $flex={6} $padding="3xs">
|
||||
<Text $size="xs" $variation="600">
|
||||
{t('Updated at')}
|
||||
{t('Name')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{isDesktop && (
|
||||
<Box $flex={1} $padding="3xs">
|
||||
<Text $size="xs" $variation="600">
|
||||
{t('Updated at')}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box $flex={1} $align="flex-end" $padding="3xs" />
|
||||
</Box>
|
||||
{/* Body */}
|
||||
{data?.pages.map((currentPage) => {
|
||||
return currentPage.results.map((doc) => (
|
||||
<DocsGridItem doc={doc} key={doc.id} />
|
||||
));
|
||||
})}
|
||||
</Box>
|
||||
<Box $flex={1} $align="flex-end" $padding="3xs" />
|
||||
</Box>
|
||||
|
||||
{loading && (
|
||||
<Box
|
||||
data-testid="docs-grid-loader"
|
||||
$padding="md"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$width="100%"
|
||||
>
|
||||
<Loader />
|
||||
{/* Body */}
|
||||
{data?.pages.map((currentPage) => {
|
||||
return currentPage.results.map((doc) => (
|
||||
<DocsGridItem doc={doc} key={doc.id} />
|
||||
));
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
{hasNextPage && !loading && (
|
||||
<InView
|
||||
data-testid="infinite-scroll-trigger"
|
||||
as="div"
|
||||
onChange={loadMore}
|
||||
>
|
||||
{!isFetching && hasNextPage && (
|
||||
<Button onClick={() => void fetchNextPage()} color="primary-text">
|
||||
{t('More docs')}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{loading && (
|
||||
<Box
|
||||
data-testid="docs-grid-loader"
|
||||
$padding="md"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$width="100%"
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
)}
|
||||
{hasNextPage && !loading && (
|
||||
<InView
|
||||
data-testid="infinite-scroll-trigger"
|
||||
as="div"
|
||||
onChange={loadMore}
|
||||
>
|
||||
{!isFetching && hasNextPage && (
|
||||
<Button onClick={() => void fetchNextPage()} color="primary-text">
|
||||
{t('More docs')}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import { useState } from 'react';
|
||||
import { useModal } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
||||
|
||||
interface DocsGridActionsProps {
|
||||
@@ -10,29 +10,31 @@ interface DocsGridActionsProps {
|
||||
|
||||
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const deleteModal = useModal();
|
||||
|
||||
if (!doc.abilities.destroy) {
|
||||
return null;
|
||||
}
|
||||
const options: DropdownMenuOption[] = [
|
||||
{
|
||||
label: t('Remove'),
|
||||
icon: 'delete',
|
||||
callback: () => deleteModal.open(),
|
||||
disabled: !doc.abilities.destroy,
|
||||
testId: `docs-grid-actions-remove-${doc.id}`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
data-testid={`docs-grid-delete-button-${doc.id}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsModalRemoveOpen(true);
|
||||
}}
|
||||
color="tertiary-text"
|
||||
icon={<span className="material-icons">delete</span>}
|
||||
size="small"
|
||||
style={{ padding: '0rem' }}
|
||||
aria-label={t('Delete the document')}
|
||||
/>
|
||||
{isModalRemoveOpen && (
|
||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||
<DropdownMenu options={options}>
|
||||
<Icon
|
||||
data-testid={`docs-grid-actions-button-${doc.id}`}
|
||||
iconName="more_horiz"
|
||||
$theme="primary"
|
||||
$variation="600"
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
||||
{deleteModal.isOpen && (
|
||||
<ModalRemoveDoc onClose={deleteModal.onClose} doc={doc} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { createGlobalStyle, css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { HEADER_HEIGHT } from '@/features/header/conf';
|
||||
|
||||
const DocsGridLoaderStyle = createGlobalStyle`
|
||||
body, main {
|
||||
overflow: hidden!important;
|
||||
overflow-y: hidden!important;
|
||||
}
|
||||
`;
|
||||
|
||||
type DocsGridLoaderProps = {
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export const DocsGridLoader = ({ isLoading }: DocsGridLoaderProps) => {
|
||||
if (!isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocsGridLoaderStyle />
|
||||
<Box
|
||||
data-testid="grid-loader"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$height="calc(100vh - 50px)"
|
||||
$width="100%"
|
||||
$maxWidth="960px"
|
||||
$background="rgba(255, 255, 255, 0.3)"
|
||||
$zIndex={998}
|
||||
$position="fixed"
|
||||
$css={css`
|
||||
top: ${HEADER_HEIGHT}px;
|
||||
`}
|
||||
>
|
||||
<Loader />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user