✨(frontend) can restore from trashbin list actions
We can now restore a doc from the trashbin list actions.
This commit is contained in:
@@ -29,10 +29,12 @@ test.describe('Doc Trashbin', () => {
|
|||||||
const row1 = await getGridRow(page, title1);
|
const row1 = await getGridRow(page, title1);
|
||||||
await clickInGridMenu(page, row1, 'Delete');
|
await clickInGridMenu(page, row1, 'Delete');
|
||||||
await page.getByRole('button', { name: 'Delete document' }).click();
|
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||||
|
await expect(row1.getByText(title1)).toBeHidden();
|
||||||
|
|
||||||
const row2 = await getGridRow(page, title2);
|
const row2 = await getGridRow(page, title2);
|
||||||
await clickInGridMenu(page, row2, 'Delete');
|
await clickInGridMenu(page, row2, 'Delete');
|
||||||
await page.getByRole('button', { name: 'Delete document' }).click();
|
await page.getByRole('button', { name: 'Delete document' }).click();
|
||||||
|
await expect(row2.getByText(title2)).toBeHidden();
|
||||||
|
|
||||||
await page.getByRole('link', { name: 'Trashbin' }).click();
|
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||||
|
|
||||||
@@ -51,5 +53,25 @@ test.describe('Doc Trashbin', () => {
|
|||||||
name: 'Open the sharing settings for the document',
|
name: 'Open the sharing settings for the document',
|
||||||
}),
|
}),
|
||||||
).toBeDisabled();
|
).toBeDisabled();
|
||||||
|
|
||||||
|
await clickInGridMenu(page, row2, 'Restore');
|
||||||
|
|
||||||
|
await expect(row2.getByText(title2)).toBeHidden();
|
||||||
|
await page.getByRole('link', { name: 'All docs' }).click();
|
||||||
|
const row2Restored = await getGridRow(page, title2);
|
||||||
|
await expect(row2Restored.getByText(title2)).toBeVisible();
|
||||||
|
await row2Restored.getByRole('link', { name: /Open document/ }).click();
|
||||||
|
|
||||||
|
await verifyDocName(page, title2);
|
||||||
|
await page.getByRole('button', { name: 'Back to homepage' }).click();
|
||||||
|
await expect(row2.getByText(title2)).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
row2.getByRole('button', {
|
||||||
|
name: 'Open the sharing settings for the document',
|
||||||
|
}),
|
||||||
|
).toBeEnabled();
|
||||||
|
|
||||||
|
await page.getByRole('link', { name: 'Trashbin' }).click();
|
||||||
|
await expect(row2.getByText(title2)).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ export const TextErrors = ({
|
|||||||
canClose = false,
|
canClose = false,
|
||||||
...textProps
|
...textProps
|
||||||
}: TextErrorsProps) => {
|
}: TextErrorsProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertStyled
|
<AlertStyled
|
||||||
canClose={canClose}
|
canClose={canClose}
|
||||||
@@ -34,31 +32,47 @@ export const TextErrors = ({
|
|||||||
icon={icon}
|
icon={icon}
|
||||||
className="--docs--text-errors"
|
className="--docs--text-errors"
|
||||||
>
|
>
|
||||||
<Box $direction="column" $gap="0.2rem">
|
<TextOnlyErrors
|
||||||
{causes &&
|
causes={causes}
|
||||||
causes.map((cause, i) => (
|
defaultMessage={defaultMessage}
|
||||||
<Text
|
{...textProps}
|
||||||
key={`causes-${i}`}
|
/>
|
||||||
$theme="danger"
|
</AlertStyled>
|
||||||
$variation="600"
|
);
|
||||||
$textAlign="center"
|
};
|
||||||
{...textProps}
|
|
||||||
>
|
|
||||||
{cause}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!causes && (
|
export const TextOnlyErrors = ({
|
||||||
|
causes,
|
||||||
|
defaultMessage,
|
||||||
|
...textProps
|
||||||
|
}: TextErrorsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box $direction="column" $gap="0.2rem">
|
||||||
|
{causes &&
|
||||||
|
causes.map((cause, i) => (
|
||||||
<Text
|
<Text
|
||||||
|
key={`causes-${i}`}
|
||||||
$theme="danger"
|
$theme="danger"
|
||||||
$variation="600"
|
$variation="600"
|
||||||
$textAlign="center"
|
$textAlign="center"
|
||||||
{...textProps}
|
{...textProps}
|
||||||
>
|
>
|
||||||
{defaultMessage || t('Something bad happens, please retry.')}
|
{cause}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
))}
|
||||||
</Box>
|
|
||||||
</AlertStyled>
|
{!causes && (
|
||||||
|
<Text
|
||||||
|
$theme="danger"
|
||||||
|
$variation="600"
|
||||||
|
$textAlign="center"
|
||||||
|
{...textProps}
|
||||||
|
>
|
||||||
|
{defaultMessage || t('Something bad happens, please retry.')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
|
|||||||
import {
|
import {
|
||||||
Fragment,
|
Fragment,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -15,7 +16,7 @@ import { useCunninghamTheme } from '@/cunningham';
|
|||||||
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||||
|
|
||||||
export type DropdownMenuOption = {
|
export type DropdownMenuOption = {
|
||||||
icon?: string;
|
icon?: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
@@ -220,7 +221,7 @@ export const DropdownMenu = ({
|
|||||||
$align="center"
|
$align="center"
|
||||||
$gap={spacingsTokens['base']}
|
$gap={spacingsTokens['base']}
|
||||||
>
|
>
|
||||||
{option.icon && (
|
{option.icon && typeof option.icon === 'string' && (
|
||||||
<Icon
|
<Icon
|
||||||
$size="20px"
|
$size="20px"
|
||||||
$theme="greyscale"
|
$theme="greyscale"
|
||||||
@@ -229,6 +230,9 @@ export const DropdownMenu = ({
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{option.icon &&
|
||||||
|
typeof option.icon !== 'string' &&
|
||||||
|
option.icon}
|
||||||
<Text $variation={isDisabled ? '400' : '1000'}>
|
<Text $variation={isDisabled ? '400' : '1000'}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './modal/AlertModal';
|
|
||||||
export * from './Box';
|
export * from './Box';
|
||||||
export * from './BoxButton';
|
export * from './BoxButton';
|
||||||
export * from './Card';
|
export * from './Card';
|
||||||
@@ -9,7 +8,7 @@ export * from './Icon';
|
|||||||
export * from './InfiniteScroll';
|
export * from './InfiniteScroll';
|
||||||
export * from './Link';
|
export * from './Link';
|
||||||
export * from './Loading';
|
export * from './Loading';
|
||||||
export * from './modal/SideModal';
|
export * from './modal';
|
||||||
export * from './separators';
|
export * from './separators';
|
||||||
export * from './Text';
|
export * from './Text';
|
||||||
export * from './TextErrors';
|
export * from './TextErrors';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
|
|
||||||
const ButtonCloseModal = (props: ButtonProps) => {
|
export const ButtonCloseModal = (props: ButtonProps) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -18,5 +18,3 @@ const ButtonCloseModal = (props: ButtonProps) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ButtonCloseModal;
|
|
||||||
|
|||||||
2
src/frontend/apps/impress/src/components/modal/index.ts
Normal file
2
src/frontend/apps/impress/src/components/modal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './AlertModal';
|
||||||
|
export * from './ButtonCloseModal';
|
||||||
@@ -16,8 +16,7 @@ import { cloneElement, isValidElement, useMemo, useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
|
||||||
import { useEditorStore } from '@/docs/doc-editor';
|
import { useEditorStore } from '@/docs/doc-editor';
|
||||||
import { Doc, useTrans } from '@/docs/doc-management';
|
import { Doc, useTrans } from '@/docs/doc-management';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export * from './useDeleteFavoriteDoc';
|
|||||||
export * from './useDoc';
|
export * from './useDoc';
|
||||||
export * from './useDocOptions';
|
export * from './useDocOptions';
|
||||||
export * from './useDocs';
|
export * from './useDocs';
|
||||||
export * from './useSubDocs';
|
|
||||||
export * from './useDuplicateDoc';
|
export * from './useDuplicateDoc';
|
||||||
|
export * from './useRestoreDoc';
|
||||||
|
export * from './useSubDocs';
|
||||||
export * from './useUpdateDoc';
|
export * from './useUpdateDoc';
|
||||||
export * from './useUpdateDocLink';
|
export * from './useUpdateDocLink';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
|||||||
import {
|
import {
|
||||||
APIError,
|
APIError,
|
||||||
APIList,
|
APIList,
|
||||||
|
InfiniteQueryConfig,
|
||||||
errorCauses,
|
errorCauses,
|
||||||
fetchAPI,
|
fetchAPI,
|
||||||
useAPIInfiniteQuery,
|
useAPIInfiniteQuery,
|
||||||
@@ -54,10 +55,10 @@ export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
|||||||
|
|
||||||
export const KEY_LIST_DOC = 'docs';
|
export const KEY_LIST_DOC = 'docs';
|
||||||
|
|
||||||
export function useDocs(
|
type UseDocsOptions = UseQueryOptions<DocsResponse, APIError, DocsResponse>;
|
||||||
params: DocsParams,
|
type UseInfiniteDocsOptions = InfiniteQueryConfig<DocsResponse>;
|
||||||
queryConfig?: UseQueryOptions<DocsResponse, APIError, DocsResponse>,
|
|
||||||
) {
|
export function useDocs(params: DocsParams, queryConfig?: UseDocsOptions) {
|
||||||
return useQuery<DocsResponse, APIError, DocsResponse>({
|
return useQuery<DocsResponse, APIError, DocsResponse>({
|
||||||
queryKey: [KEY_LIST_DOC, params],
|
queryKey: [KEY_LIST_DOC, params],
|
||||||
queryFn: () => getDocs(params),
|
queryFn: () => getDocs(params),
|
||||||
@@ -65,6 +66,9 @@ export function useDocs(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useInfiniteDocs = (params: DocsParams) => {
|
export const useInfiniteDocs = (
|
||||||
return useAPIInfiniteQuery(KEY_LIST_DOC, getDocs, params);
|
params: DocsParams,
|
||||||
|
queryConfig?: UseInfiniteDocsOptions,
|
||||||
|
) => {
|
||||||
|
return useAPIInfiniteQuery(KEY_LIST_DOC, getDocs, params, queryConfig);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
|
||||||
import { KEY_LIST_DOC } from './useDocs';
|
|
||||||
|
|
||||||
interface RemoveDocProps {
|
interface RemoveDocProps {
|
||||||
docId: string;
|
docId: string;
|
||||||
}
|
}
|
||||||
@@ -24,14 +22,22 @@ export const removeDoc = async ({ docId }: RemoveDocProps): Promise<void> => {
|
|||||||
|
|
||||||
type UseRemoveDocOptions = UseMutationOptions<void, APIError, RemoveDocProps>;
|
type UseRemoveDocOptions = UseMutationOptions<void, APIError, RemoveDocProps>;
|
||||||
|
|
||||||
export const useRemoveDoc = (options?: UseRemoveDocOptions) => {
|
export const useRemoveDoc = ({
|
||||||
|
listInvalidQueries,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
listInvalidQueries?: string[];
|
||||||
|
options?: UseRemoveDocOptions;
|
||||||
|
}) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<void, APIError, RemoveDocProps>({
|
return useMutation<void, APIError, RemoveDocProps>({
|
||||||
mutationFn: removeDoc,
|
mutationFn: removeDoc,
|
||||||
...options,
|
...options,
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
void queryClient.invalidateQueries({
|
listInvalidQueries?.forEach((queryKey) => {
|
||||||
queryKey: [KEY_LIST_DOC],
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [queryKey],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (options?.onSuccess) {
|
if (options?.onSuccess) {
|
||||||
void options.onSuccess(data, variables, context);
|
void options.onSuccess(data, variables, context);
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
UseMutationOptions,
|
||||||
|
useMutation,
|
||||||
|
useQueryClient,
|
||||||
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
|
||||||
|
interface RestoreDocProps {
|
||||||
|
docId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const restoreDoc = async ({ docId }: RestoreDocProps): Promise<void> => {
|
||||||
|
const response = await fetchAPI(`documents/${docId}/restore/`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to restore the doc',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseRestoreDocOptions = UseMutationOptions<void, APIError, RestoreDocProps>;
|
||||||
|
|
||||||
|
export const useRestoreDoc = ({
|
||||||
|
listInvalidQueries,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
listInvalidQueries?: string[];
|
||||||
|
options?: UseRestoreDocOptions;
|
||||||
|
}) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, APIError, RestoreDocProps>({
|
||||||
|
mutationFn: restoreDoc,
|
||||||
|
...options,
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
listInvalidQueries?.forEach((queryKey) => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [queryKey],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (options?.onSuccess) {
|
||||||
|
void options.onSuccess(data, variables, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error, variables, context) => {
|
||||||
|
if (options?.onError) {
|
||||||
|
void options.onError(error, variables, context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -9,10 +9,13 @@ import { usePathname } from 'next/navigation';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Box, Text, TextErrors } from '@/components';
|
import { Box, ButtonCloseModal, Text, TextErrors } from '@/components';
|
||||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
import { useConfig } from '@/core';
|
||||||
|
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
|
||||||
|
|
||||||
|
import { KEY_LIST_DOC } from '../api/useDocs';
|
||||||
import { useRemoveDoc } from '../api/useRemoveDoc';
|
import { useRemoveDoc } from '../api/useRemoveDoc';
|
||||||
|
import { useDocUtils } from '../hooks';
|
||||||
import { Doc } from '../types';
|
import { Doc } from '../types';
|
||||||
|
|
||||||
interface ModalRemoveDocProps {
|
interface ModalRemoveDocProps {
|
||||||
@@ -28,25 +31,31 @@ export const ModalRemoveDoc = ({
|
|||||||
}: ModalRemoveDocProps) => {
|
}: ModalRemoveDocProps) => {
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { data: config } = useConfig();
|
||||||
|
const trashBinCutoffDays = config?.TRASHBIN_CUTOFF_DAYS || 30;
|
||||||
const { push } = useRouter();
|
const { push } = useRouter();
|
||||||
|
const { hasChildren } = useDocUtils(doc);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const {
|
const {
|
||||||
mutate: removeDoc,
|
mutate: removeDoc,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useRemoveDoc({
|
} = useRemoveDoc({
|
||||||
onSuccess: () => {
|
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN],
|
||||||
if (onSuccess) {
|
options: {
|
||||||
onSuccess(doc);
|
onSuccess: () => {
|
||||||
} else if (pathname === '/') {
|
if (onSuccess) {
|
||||||
onClose();
|
onSuccess(doc);
|
||||||
} else {
|
} else if (pathname === '/') {
|
||||||
void push('/');
|
onClose();
|
||||||
}
|
} else {
|
||||||
|
void push('/');
|
||||||
|
}
|
||||||
|
|
||||||
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -109,10 +118,18 @@ export const ModalRemoveDoc = ({
|
|||||||
<Box className="--docs--modal-remove-doc">
|
<Box className="--docs--modal-remove-doc">
|
||||||
{!isError && (
|
{!isError && (
|
||||||
<Text $size="sm" $variation="600" $display="inline-block" as="p">
|
<Text $size="sm" $variation="600" $display="inline-block" as="p">
|
||||||
<Trans t={t}>
|
{hasChildren ? (
|
||||||
This document and <strong>any sub-documents</strong> will be
|
<Trans t={t}>
|
||||||
permanently deleted. This action is irreversible.
|
This document and <strong>any sub-documents</strong> will be
|
||||||
</Trans>
|
placed in the trashbin. You can restore it within{' '}
|
||||||
|
{{ days: trashBinCutoffDays }} days.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
t(
|
||||||
|
'This document will be placed in the trashbin. You can restore it within {{days}} days.',
|
||||||
|
{ days: trashBinCutoffDays },
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
|
||||||
import { QuickSearch } from '@/components/quick-search';
|
import { QuickSearch } from '@/components/quick-search';
|
||||||
import { Doc, useDocUtils } from '@/docs/doc-management';
|
import { Doc, useDocUtils } from '@/docs/doc-management';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { createGlobalStyle, css } from 'styled-components';
|
import { createGlobalStyle, css } from 'styled-components';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import { Box, HorizontalSeparator, Text } from '@/components';
|
import { Box, ButtonCloseModal, HorizontalSeparator, Text } from '@/components';
|
||||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
|
||||||
import {
|
import {
|
||||||
QuickSearch,
|
QuickSearch,
|
||||||
QuickSearchData,
|
QuickSearchData,
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { useState } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { createGlobalStyle, css } from 'styled-components';
|
import { createGlobalStyle, css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, ButtonCloseModal, Text } from '@/components';
|
||||||
import ButtonCloseModal from '@/components/modal/ButtonCloseModal';
|
|
||||||
import { DocEditor } from '@/docs/doc-editor';
|
import { DocEditor } from '@/docs/doc-editor';
|
||||||
import { Doc } from '@/docs/doc-management';
|
import { Doc } from '@/docs/doc-management';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './useDocsTrashbin';
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import {
|
||||||
|
APIError,
|
||||||
|
APIList,
|
||||||
|
InfiniteQueryConfig,
|
||||||
|
errorCauses,
|
||||||
|
fetchAPI,
|
||||||
|
useAPIInfiniteQuery,
|
||||||
|
} from '@/api';
|
||||||
|
import { Doc, DocsResponse } from '@/docs/doc-management';
|
||||||
|
|
||||||
|
export type DocsTrashbinParams = {
|
||||||
|
page: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DocsTrashbinResponse = APIList<Doc>;
|
||||||
|
export const getDocsTrashbin = async (
|
||||||
|
params: DocsTrashbinParams,
|
||||||
|
): Promise<DocsTrashbinResponse> => {
|
||||||
|
const response = await fetchAPI(`documents/trashbin/?page=${params.page}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError('Failed to get the docs', await errorCauses(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json() as Promise<DocsTrashbinResponse>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KEY_LIST_DOC_TRASHBIN = 'docs_trashbin';
|
||||||
|
|
||||||
|
type UseDocsTrashbinOptions = UseQueryOptions<
|
||||||
|
DocsResponse,
|
||||||
|
APIError,
|
||||||
|
DocsResponse
|
||||||
|
>;
|
||||||
|
type UseInfiniteDocsTrashbinOptions = InfiniteQueryConfig<DocsTrashbinResponse>;
|
||||||
|
|
||||||
|
export function useDocsTrashbin(
|
||||||
|
params: DocsTrashbinParams,
|
||||||
|
queryConfig?: UseDocsTrashbinOptions,
|
||||||
|
) {
|
||||||
|
return useQuery<DocsTrashbinResponse, APIError, DocsTrashbinResponse>({
|
||||||
|
queryKey: [KEY_LIST_DOC_TRASHBIN, params],
|
||||||
|
queryFn: () => getDocsTrashbin(params),
|
||||||
|
...queryConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInfiniteDocsTrashbin = (
|
||||||
|
params: DocsTrashbinParams,
|
||||||
|
queryConfig?: UseInfiniteDocsTrashbinOptions,
|
||||||
|
) => {
|
||||||
|
return useAPIInfiniteQuery(
|
||||||
|
KEY_LIST_DOC_TRASHBIN,
|
||||||
|
getDocsTrashbin,
|
||||||
|
params,
|
||||||
|
queryConfig,
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||||
|
import { Doc, KEY_LIST_DOC, useRestoreDoc } from '@/docs/doc-management';
|
||||||
|
|
||||||
|
import { KEY_LIST_DOC_TRASHBIN } from '../api';
|
||||||
|
|
||||||
|
interface DocsGridTrashbinActionsProps {
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocsGridTrashbinActions = ({
|
||||||
|
doc,
|
||||||
|
}: DocsGridTrashbinActionsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { toast } = useToastProvider();
|
||||||
|
const { mutate: restoreDoc, error } = useRestoreDoc({
|
||||||
|
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN],
|
||||||
|
options: {
|
||||||
|
onSuccess: (_data) => {
|
||||||
|
toast(t('The document has been restored.'), VariantType.SUCCESS, {
|
||||||
|
duration: 4000,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast(
|
||||||
|
t('An error occurred while restoring the document: {{error}}', {
|
||||||
|
error: error?.message,
|
||||||
|
}),
|
||||||
|
VariantType.ERROR,
|
||||||
|
{
|
||||||
|
duration: 4000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const options: DropdownMenuOption[] = [
|
||||||
|
{
|
||||||
|
label: t('Restore'),
|
||||||
|
icon: (
|
||||||
|
<Icon
|
||||||
|
$size="20px"
|
||||||
|
$theme="greyscale"
|
||||||
|
$variation="1000"
|
||||||
|
iconName="undo"
|
||||||
|
aria-hidden="true"
|
||||||
|
variant="symbols-outlined"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
callback: () => {
|
||||||
|
restoreDoc({
|
||||||
|
docId: doc.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
testId: `docs-grid-actions-restore-${doc.id}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const documentTitle = doc.title || t('Untitled document');
|
||||||
|
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
||||||
|
title: documentTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu
|
||||||
|
options={options}
|
||||||
|
label={menuLabel}
|
||||||
|
aria-label={t('More options')}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
data-testid={`docs-grid-actions-button-${doc.id}`}
|
||||||
|
iconName="more_horiz"
|
||||||
|
$theme="primary"
|
||||||
|
$variation="600"
|
||||||
|
$css={css`
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
export * from './api';
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|||||||
Reference in New Issue
Block a user