💄(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
@@ -88,6 +88,8 @@ and this project adheres to
|
|||||||
- ✨(frontend) config endpoint #424
|
- ✨(frontend) config endpoint #424
|
||||||
- ✨(frontend) add sentry #424
|
- ✨(frontend) add sentry #424
|
||||||
- ✨(frontend) add crisp chatbot #450
|
- ✨(frontend) add crisp chatbot #450
|
||||||
|
- 💄(frontend) update DocsGridOptions component #432
|
||||||
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -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.describe('Documents Grid', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
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 { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, BoxType } from './Box';
|
import { Box, BoxType } from './Box';
|
||||||
|
|
||||||
export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
export type BoxButtonType = BoxType & {
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styleless button that extends the Box component.
|
* Styleless button that extends the Box component.
|
||||||
@@ -18,7 +22,7 @@ export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
|||||||
* </BoxButton>
|
* </BoxButton>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
||||||
({ $css, ...props }, ref) => {
|
({ $css, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -28,14 +32,24 @@ const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
|||||||
$margin="none"
|
$margin="none"
|
||||||
$padding="none"
|
$padding="none"
|
||||||
$css={css`
|
$css={css`
|
||||||
cursor: pointer;
|
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|
||||||
|
color: ${props.disabled
|
||||||
|
? 'var(--c--theme--colors--greyscale-400) !important'
|
||||||
|
: 'inherit'};
|
||||||
${$css || ''}
|
${$css || ''}
|
||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
|
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.onClick?.(event);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import React, {
|
import { PropsWithChildren, ReactNode, useEffect, useState } from 'react';
|
||||||
PropsWithChildren,
|
|
||||||
ReactNode,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Button, DialogTrigger, Popover } from 'react-aria-components';
|
import { Button, DialogTrigger, Popover } from 'react-aria-components';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
@@ -11,7 +6,7 @@ const StyledPopover = styled(Popover)`
|
|||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid #dddddd;
|
border: 1px solid #dddddd;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
@@ -29,7 +24,7 @@ const StyledButton = styled(Button)`
|
|||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface DropButtonProps {
|
export interface DropButtonProps {
|
||||||
button: ReactNode;
|
button: ReactNode;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onOpenChange?: (isOpen: boolean) => void;
|
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 './BoxButton';
|
||||||
export * from './Card';
|
export * from './Card';
|
||||||
export * from './DropButton';
|
export * from './DropButton';
|
||||||
|
export * from './DropdownMenu';
|
||||||
export * from './Icon';
|
export * from './Icon';
|
||||||
export * from './InfiniteScroll';
|
export * from './InfiniteScroll';
|
||||||
export * from './Link';
|
export * from './Link';
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ import {
|
|||||||
VariantType,
|
VariantType,
|
||||||
useToastProvider,
|
useToastProvider,
|
||||||
} from '@openfun/cunningham-react';
|
} from '@openfun/cunningham-react';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Box, Text, TextErrors } from '@/components';
|
import { Box, Text, TextErrors } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham/';
|
|
||||||
|
|
||||||
import { useRemoveDoc } from '../api/useRemoveDoc';
|
import { useRemoveDoc } from '../api/useRemoveDoc';
|
||||||
import IconDoc from '../assets/icon-doc.svg';
|
|
||||||
import { Doc } from '../types';
|
import { Doc } from '../types';
|
||||||
|
|
||||||
interface ModalRemoveDocProps {
|
interface ModalRemoveDocProps {
|
||||||
@@ -22,13 +21,13 @@ interface ModalRemoveDocProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: removeDoc,
|
mutate: removeDoc,
|
||||||
|
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useRemoveDoc({
|
} = useRemoveDoc({
|
||||||
@@ -36,7 +35,11 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
|||||||
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
void push('/');
|
if (pathname === '/') {
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
void push('/');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,7 +62,7 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
|||||||
rightActions={
|
rightActions={
|
||||||
<Button
|
<Button
|
||||||
aria-label={t('Confirm deletion')}
|
aria-label={t('Confirm deletion')}
|
||||||
color="primary"
|
color="danger"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
removeDoc({
|
removeDoc({
|
||||||
@@ -97,32 +100,6 @@ export const ModalRemoveDoc = ({ onClose, doc }: ModalRemoveDocProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isError && <TextErrors causes={error.cause} />}
|
{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>
|
</Box>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,16 +8,23 @@ import { useResponsiveStore } from '@/stores';
|
|||||||
import { useInfiniteDocs } from '../../doc-management';
|
import { useInfiniteDocs } from '../../doc-management';
|
||||||
|
|
||||||
import { DocsGridItem } from './DocsGridItem';
|
import { DocsGridItem } from './DocsGridItem';
|
||||||
|
import { DocsGridLoader } from './DocsGridLoader';
|
||||||
|
|
||||||
export const DocsGrid = () => {
|
export const DocsGrid = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
|
|
||||||
const { data, isFetching, isLoading, fetchNextPage, hasNextPage } =
|
const {
|
||||||
useInfiniteDocs({
|
data,
|
||||||
page: 1,
|
isFetching,
|
||||||
});
|
isRefetching,
|
||||||
|
isLoading,
|
||||||
|
fetchNextPage,
|
||||||
|
hasNextPage,
|
||||||
|
} = useInfiniteDocs({
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
const loading = isFetching || isLoading;
|
const loading = isFetching || isLoading;
|
||||||
|
|
||||||
const loadMore = (inView: boolean) => {
|
const loadMore = (inView: boolean) => {
|
||||||
@@ -28,65 +35,69 @@ export const DocsGrid = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card data-testid="docs-grid" $padding="md" $width="100%" $maxWidth="960px">
|
<Box $position="relative" $width="100%" $maxWidth="960px">
|
||||||
<Text
|
<DocsGridLoader isLoading={isRefetching} />
|
||||||
as="h4"
|
<Card data-testid="docs-grid" $padding="md">
|
||||||
$size="h4"
|
<Text
|
||||||
$weight="700"
|
as="h4"
|
||||||
$margin={{ top: '0px', bottom: 'xs' }}
|
$size="h4"
|
||||||
>
|
$weight="700"
|
||||||
{t('All docs')}
|
$margin={{ top: '0px', bottom: 'xs' }}
|
||||||
</Text>
|
>
|
||||||
|
{t('All docs')}
|
||||||
|
</Text>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Box $direction="row" $padding="xs" data-testid="docs-grid-header">
|
<Box $direction="row" $padding="xs" data-testid="docs-grid-header">
|
||||||
<Box $flex={6} $padding="3xs">
|
<Box $flex={6} $padding="3xs">
|
||||||
<Text $size="xs" $variation="600">
|
|
||||||
{t('Name')}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{isDesktop && (
|
|
||||||
<Box $flex={1} $padding="3xs">
|
|
||||||
<Text $size="xs" $variation="600">
|
<Text $size="xs" $variation="600">
|
||||||
{t('Updated at')}
|
{t('Name')}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</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 $flex={1} $align="flex-end" $padding="3xs" />
|
||||||
</Box>
|
</Box>
|
||||||
{/* Body */}
|
|
||||||
{data?.pages.map((currentPage) => {
|
|
||||||
return currentPage.results.map((doc) => (
|
|
||||||
<DocsGridItem doc={doc} key={doc.id} />
|
|
||||||
));
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{loading && (
|
{/* Body */}
|
||||||
<Box
|
{data?.pages.map((currentPage) => {
|
||||||
data-testid="docs-grid-loader"
|
return currentPage.results.map((doc) => (
|
||||||
$padding="md"
|
<DocsGridItem doc={doc} key={doc.id} />
|
||||||
$align="center"
|
));
|
||||||
$justify="center"
|
})}
|
||||||
$width="100%"
|
|
||||||
>
|
|
||||||
<Loader />
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
{hasNextPage && !loading && (
|
{loading && (
|
||||||
<InView
|
<Box
|
||||||
data-testid="infinite-scroll-trigger"
|
data-testid="docs-grid-loader"
|
||||||
as="div"
|
$padding="md"
|
||||||
onChange={loadMore}
|
$align="center"
|
||||||
>
|
$justify="center"
|
||||||
{!isFetching && hasNextPage && (
|
$width="100%"
|
||||||
<Button onClick={() => void fetchNextPage()} color="primary-text">
|
>
|
||||||
{t('More docs')}
|
<Loader />
|
||||||
</Button>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</InView>
|
{hasNextPage && !loading && (
|
||||||
)}
|
<InView
|
||||||
</Card>
|
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 { useModal } from '@openfun/cunningham-react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
interface DocsGridActionsProps {
|
interface DocsGridActionsProps {
|
||||||
@@ -10,29 +10,31 @@ interface DocsGridActionsProps {
|
|||||||
|
|
||||||
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
const deleteModal = useModal();
|
||||||
|
|
||||||
if (!doc.abilities.destroy) {
|
const options: DropdownMenuOption[] = [
|
||||||
return null;
|
{
|
||||||
}
|
label: t('Remove'),
|
||||||
|
icon: 'delete',
|
||||||
|
callback: () => deleteModal.open(),
|
||||||
|
disabled: !doc.abilities.destroy,
|
||||||
|
testId: `docs-grid-actions-remove-${doc.id}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<DropdownMenu options={options}>
|
||||||
data-testid={`docs-grid-delete-button-${doc.id}`}
|
<Icon
|
||||||
onClick={(event) => {
|
data-testid={`docs-grid-actions-button-${doc.id}`}
|
||||||
event.preventDefault();
|
iconName="more_horiz"
|
||||||
event.stopPropagation();
|
$theme="primary"
|
||||||
setIsModalRemoveOpen(true);
|
$variation="600"
|
||||||
}}
|
/>
|
||||||
color="tertiary-text"
|
</DropdownMenu>
|
||||||
icon={<span className="material-icons">delete</span>}
|
|
||||||
size="small"
|
{deleteModal.isOpen && (
|
||||||
style={{ padding: '0rem' }}
|
<ModalRemoveDoc onClose={deleteModal.onClose} doc={doc} />
|
||||||
aria-label={t('Delete the document')}
|
|
||||||
/>
|
|
||||||
{isModalRemoveOpen && (
|
|
||||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} 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