This commit is contained in:
Anthony LC
2026-08-14 15:37:07 +02:00
parent 0b933ed3a2
commit d602e9fa6c
18 changed files with 389 additions and 84 deletions
+5 -3
View File
@@ -1338,10 +1338,12 @@ class DocumentViewSet(
document=duplicated_document,
)
return drf_response.Response(
{"id": str(duplicated_document.id)}, status=status.HTTP_201_CREATED
serializer = serializers.DocumentSerializer(
duplicated_document, context=self.get_serializer_context()
)
return drf_response.Response(serializer.data, status=status.HTTP_201_CREATED)
def _duplicate_document(
self,
document_to_duplicate,
@@ -1369,7 +1371,7 @@ class DocumentViewSet(
user_role = document_to_duplicate.get_role(user)
is_owner_or_admin = user_role in models.PRIVILEGED_ROLES
base64_yjs_content = document_to_duplicate.content
base64_yjs_content = document_to_duplicate.content or ""
# Duplicate the document instance
link_kwargs = (
@@ -1,5 +1,8 @@
import { Button } from '@gouvfr-lasuite/cunningham-react';
import Link from 'next/link';
import styled, { RuleSet } from 'styled-components';
import { useRouter } from 'next/router';
import { type ComponentProps } from 'react';
import styled, { type RuleSet } from 'styled-components';
export interface LinkProps {
$css?: string | RuleSet<object>;
@@ -14,3 +17,35 @@ export const StyledLink = styled(Link)<LinkProps>`
display: flex;
${({ $css }) => $css && (typeof $css === 'string' ? `${$css};` : $css)}
`;
type ButtonLinkProps = ComponentProps<typeof Button> & {
href: string;
};
export const ButtonLink = ({
children,
onClick,
ref,
...props
}: ButtonLinkProps) => {
const router = useRouter();
return (
<Button
ref={ref}
onClick={(e) => {
if (!e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
if (props.href) {
void router.push(props.href);
}
}
onClick?.(e as React.MouseEvent<HTMLButtonElement, MouseEvent>);
}}
{...props}
>
{children}
</Button>
);
};
@@ -57,6 +57,18 @@
height: auto;
}
/**
* Checkbox
*/
.c__checkbox:focus-within {
border-color: transparent;
box-shadow: none;
}
.c__checkbox .c__checkbox__wrapper:focus-within {
outline: none;
}
/**
* Modal
*/
@@ -273,7 +273,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
>
{t('Export')}
</Text>
<Box $position="absolute" $css="top: 4px; right: 4px;">
<Box $position="absolute" $css="top: 8px; right: 8px;">
<ButtonCloseModal
aria-label={t('Close the download modal')}
onClick={() => onClose()}
@@ -188,6 +188,11 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
$align="center"
$gap="4px"
$minHeight="40px"
$css={css`
&:focus-within {
outline: none;
}
`}
>
{!isTopRoot && <DocTitleEmojiPicker doc={doc} />}
{/**
@@ -22,7 +22,7 @@ import LeaveSVG from '@/assets/icons/ui-kit/leave.svg';
import MarkdownCopySVG from '@/assets/icons/ui-kit/markdown_copy.svg';
import MoreSVG from '@/assets/icons/ui-kit/more_horiz.svg';
import {
Doc,
type Doc,
KEY_DOC,
KEY_LIST_DOC,
KEY_LIST_FAVORITE_DOC,
@@ -32,6 +32,7 @@ import {
useDocUtils,
useDuplicateDoc,
} from '@/docs/doc-management';
import { ConfirmationDuplicateModal } from '@/docs/doc-management/components/ConfirmationDuplicateModal';
import { usePresenterStore } from '@/docs/doc-presenter/stores';
import { useAuth } from '@/features/auth';
import { useFocusStore, useResponsiveStore } from '@/stores';
@@ -96,6 +97,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { authenticated } = useAuth();
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
const [openDropdown, setOpenDropdown] = useState(false);
const [isModalDuplicateOpen, setIsModalDuplicateOpen] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
@@ -192,11 +194,14 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
isDisabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
docId: doc.id,
with_accesses: false,
canSave: doc.abilities.partial_update,
});
if (doc.numchild) {
setIsModalDuplicateOpen(true);
} else {
duplicateDoc({
docId: doc.id,
canSave: doc.abilities.partial_update,
});
}
},
isHidden: !doc.abilities.duplicate,
showSeparator: true,
@@ -247,6 +252,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
/>
</DropdownMenu>
{isModalDuplicateOpen && (
<ConfirmationDuplicateModal
onClose={() => {
setIsModalDuplicateOpen(false);
restoreFocus();
}}
doc={doc}
/>
)}
{isModalExportOpen && ModalExport && (
<ModalExport
onClose={() => {
@@ -3,7 +3,8 @@ import {
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import {
UseMutationOptions,
type InfiniteData,
type UseMutationOptions,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
@@ -18,22 +19,24 @@ import { useProviderStore } from '../stores';
import { Doc } from '../types';
import { useDocContentUpdate } from './useDocContentUpdate';
import { KEY_LIST_DOC } from './useDocs';
import { DocsParams, DocsResponse, KEY_LIST_DOC } from './useDocs';
interface DuplicateDocPayload {
docId: string;
with_accesses?: boolean;
with_descendants?: boolean;
}
type DuplicateDocResponse = Pick<Doc, 'id'>;
type DuplicateDocResponse = Doc;
export const duplicateDoc = async ({
docId,
with_accesses,
with_accesses = false,
with_descendants = true,
}: DuplicateDocPayload): Promise<DuplicateDocResponse> => {
const response = await fetchAPI(`documents/${docId}/duplicate/`, {
method: 'POST',
body: JSON.stringify({ with_accesses }),
body: JSON.stringify({ with_accesses, with_descendants }),
});
if (!response.ok) {
@@ -84,14 +87,36 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
return await duplicateDoc(variables);
},
onSuccess: (data, variables, onMutateResult, context) => {
void queryClient.resetQueries({
queryKey: [KEY_LIST_DOC],
});
// Add the duplicated document to the list of documents in the cache
// It avoids the need to refetch the list of documents after duplicating a document
queryClient.setQueriesData<InfiniteData<DocsResponse>>(
{
queryKey: [KEY_LIST_DOC],
predicate: (query) => {
const params = query.queryKey[1] as DocsParams | undefined;
return params?.is_creator_me !== false;
},
},
(oldData) => {
if (!oldData) {
return oldData;
}
const message = t('Document duplicated successfully!');
toast(message, VariantType.SUCCESS, {
duration: 3000,
});
const [firstPage, ...restPages] = oldData.pages;
return {
...oldData,
pages: [
{
...firstPage,
count: firstPage.count + 1,
results: [data, ...firstPage.results],
},
...restPages,
],
};
},
);
void options?.onSuccess?.(data, variables, onMutateResult, context);
},
@@ -1,5 +1,5 @@
import {
UseMutationOptions,
type UseMutationOptions,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
@@ -0,0 +1,175 @@
import {
Button,
Checkbox,
Modal,
ModalSize,
VariantType,
useToastProvider,
} from '@gouvfr-lasuite/cunningham-react';
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { Box, ButtonCloseModal, ButtonLink, Text } from '@/components';
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid/api/useDocsTrashbin';
import { KEY_LIST_DOC, useDuplicateDoc } from '../api';
import { useRemoveDoc } from '../api/useRemoveDoc';
import { type Doc } from '../types';
const ModalStyle = createGlobalStyle`
.c__modal__footer {
margin-top: 0;
}
`;
interface ConfirmationDuplicateModalProps {
doc: Doc;
onClose: () => void;
}
export const ConfirmationDuplicateModal = ({
doc,
onClose,
}: ConfirmationDuplicateModalProps) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const [isWithSubdocs, setIsWithSubdocs] = useState(true);
const { mutate: duplicateDoc } = useDuplicateDoc({
onSuccess: (data) => {
onClose();
const onCloseToast = (
e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
) => {
const toastEl = e.currentTarget.closest<HTMLElement>('[role="alert"]');
if (toastEl) {
toastEl.style.display = 'none';
}
};
toast(t('Document duplicated to My docs'), VariantType.INFO, {
duration: 10000,
actions: <ToastActions docId={data.id} onClose={onCloseToast} />,
});
},
});
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
onClose={onClose}
aria-label={t('Confirmation to duplicate the document')}
aria-labelledby="modal-duplicate-doc-title"
aria-describedby="modal-duplicate-doc-desc"
rightActions={
<>
<Button
aria-label={t('Cancel the duplicate action')}
variant="secondary"
fullWidth
autoFocus
onClick={onClose}
>
{t('Cancel')}
</Button>
<Button
aria-label={t('Confirm the duplicate action')}
color="error"
fullWidth
onClick={() => {
duplicateDoc({
docId: doc.id,
with_descendants: isWithSubdocs,
canSave: doc.abilities.partial_update,
});
}}
>
{t('Duplicate')}
</Button>
</>
}
size={ModalSize.SMALL}
title={
<>
<Text
$size="h6"
as="h2"
id="modal-duplicate-doc-title"
$margin="0"
$align="flex-start"
>
{t('Duplicate')}
</Text>
<Box $position="absolute" $css="top: 8px; right: 8px;">
<ButtonCloseModal
aria-label={t('Close the duplicate modal')}
onClick={onClose}
/>
</Box>
</>
}
>
<ModalStyle />
<Text
id="modal-duplicate-doc-desc"
className="--docs--modal-duplicate-doc"
$size="sm"
$variation="secondary"
as="p"
$margin="0"
>
{t('The copy will be private and added to My docs.')}
</Text>
<Box $margin={{ vertical: 'base' }}>
<Checkbox
label={t('Duplicate subdocs')}
checked={isWithSubdocs}
onChange={(e) => setIsWithSubdocs(e.target.checked)}
/>
</Box>
</Modal>
);
};
const ToastActions = ({
docId,
onClose,
}: {
docId: string;
onClose: (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => void;
}) => {
const { t } = useTranslation();
const { mutate: removeDoc } = useRemoveDoc({
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_DOC_TRASHBIN],
});
const openRef = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
// When the toast is displayed, we want to focus the "Open" button for accessibility reasons.
useEffect(() => {
openRef.current?.focus();
}, []);
return (
<Box $direction="row" $gap="xs">
<ButtonLink
ref={openRef}
variant="tertiary"
href={`/docs/${docId}`}
onClick={onClose}
>
{t('Open')}
</ButtonLink>
<Button
variant="tertiary"
onClick={(e) => {
removeDoc({ docId });
onClose(e);
}}
>
{t('Undo')}
</Button>
</Box>
);
};
@@ -12,7 +12,6 @@ import { Trans, useTranslation } from 'react-i18next';
import { Box, ButtonCloseModal, Text, TextErrors } from '@/components';
import { useConfig } from '@/core';
import { KEY_LIST_DOC_TRASHBIN } from '@/docs/docs-grid';
import { useKeyboardAction } from '@/hooks';
import { KEY_DOC, KEY_LIST_FAVORITE_DOC } from '../api';
import { KEY_LIST_DOC } from '../api/useDocs';
@@ -73,25 +72,16 @@ export const ModalRemoveDoc = ({
return () => cancelAnimationFrame(id);
}, []);
const keyboardAction = useKeyboardAction();
const handleClose = () => {
onClose();
};
const handleDelete = () => {
removeDoc({ docId: doc.id });
};
const handleCloseKeyDown = keyboardAction(handleClose);
const handleDeleteKeyDown = keyboardAction(handleDelete);
return (
<Modal
isOpen
closeOnClickOutside
hideCloseButton
onClose={handleClose}
onClose={onClose}
aria-label={t('Delete a doc')}
rightActions={
<>
@@ -101,8 +91,7 @@ export const ModalRemoveDoc = ({
variant="secondary"
fullWidth
autoFocus
onClick={handleClose}
onKeyDown={handleCloseKeyDown}
onClick={onClose}
>
{t('Cancel')}
</Button>
@@ -111,7 +100,6 @@ export const ModalRemoveDoc = ({
color="error"
fullWidth
onClick={handleDelete}
onKeyDown={handleDeleteKeyDown}
>
{t('Delete')}
</Button>
@@ -129,11 +117,10 @@ export const ModalRemoveDoc = ({
>
{t('Delete a doc')}
</Text>
<Box $position="absolute" $css="top: 4px; right: 4px;">
<Box $position="absolute" $css="top: 8px; right: 8px;">
<ButtonCloseModal
aria-label={t('Close the delete modal')}
onClick={handleClose}
onKeyDown={handleCloseKeyDown}
onClick={onClose}
/>
</Box>
</>
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Text } from '@/components';
import { ButtonLink, Text } from '@/components';
import { useConfig } from '@/core/config/api/useConfig';
import { useLeftPanelStore } from '@/features/left-panel/stores/useLeftPanelStore';
import ArrowDownIcon from '@/icons/arrow-drop-down.svg';
@@ -22,7 +22,6 @@ interface NewDocButtonProps {
}
export const NewDocButton = ({ onClose }: NewDocButtonProps) => {
const router = useRouter();
const { t } = useTranslation();
const { currentDoc } = useDocStore();
const { data: config } = useConfig();
@@ -30,15 +29,11 @@ export const NewDocButton = ({ onClose }: NewDocButtonProps) => {
return (
<>
<Button
<ButtonLink
href="/docs/new"
data-testid="new-doc-button"
color="brand"
onClick={(e) => {
if (!e.ctrlKey && !e.metaKey && !e.shiftKey) {
e.preventDefault();
void router.push('/docs/new');
}
onClick={() => {
onClose?.();
}}
icon={<PlusIcon aria-hidden="true" width={24} height={24} />}
@@ -51,7 +46,7 @@ export const NewDocButton = ({ onClose }: NewDocButtonProps) => {
<Text $withThemeInherited $size="md" $weight="500">
{t('New')}
</Text>
</Button>
</ButtonLink>
{isDropdownEnabled && <DropdownArrow />}
</>
);
@@ -109,7 +109,7 @@ const DocSearchModalGlobal = ({
'Search documents by name, navigate using arrows, and select a result with Enter.',
)}
</Text>
<Box $position="absolute" $css="top: 4px; right: 4px;">
<Box $position="absolute" $css="top: 8px; right: 8px;">
<ButtonCloseModal
aria-label={t('Close the search modal')}
onClick={modalProps.onClose}
@@ -80,7 +80,7 @@ export const ConfirmationLeaveModal = ({
>
{t('Leave a doc')}
</Text>
<Box $position="absolute" $css="top: 4px; right: 4px;">
<Box $position="absolute" $css="top: 8px; right: 8px;">
<ButtonCloseModal
aria-label={t('Close the leave modal')}
onClick={onClose}
@@ -19,7 +19,7 @@ import {
useTrans,
} from '@/docs/doc-management';
import { useLeftPanelStore } from '@/features/left-panel';
import { useResponsiveStore } from '@/stores';
import { useFocusStore, useResponsiveStore } from '@/stores';
import { isDocNode } from '../utils';
@@ -108,6 +108,7 @@ const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => {
const [menuOpen, setMenuOpen] = useState(false);
const router = useRouter();
const { closePanel } = useLeftPanelStore();
const { addLastFocus } = useFocusStore();
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
const displayTitle = titleWithoutEmoji || untitledDocument;
@@ -148,6 +149,7 @@ const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => {
const isCurrentPage = router.query?.id === doc.id;
const isDeleted = !!doc.deleted_at;
const actionsRef = useRef<HTMLDivElement>(null);
const subPageItemRef = useRef<HTMLAnchorElement>(null);
const buttonOptionRef = useRef<ButtonElement | null>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -168,15 +170,18 @@ const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => {
const handleActionsOpenChange = (isOpen: boolean) => {
setMenuOpen(isOpen);
// When the menu closes (via Escape or activating an option),
// return focus to the tree item so focus is not lost.
if (!isOpen) {
node.focus();
// When the actions menu opens, we save the last focused element
// to restore focus later when the menu closes or actions are completed.
const el =
subPageItemRef?.current?.closest<HTMLDivElement>('.c__tree-view--row');
if (isOpen && el) {
addLastFocus(el);
}
};
return (
<StyledLink
ref={subPageItemRef}
className="--docs-sub-page-item"
/**
* Conflict with the react-arborist DND.
@@ -30,6 +30,7 @@ import {
} from '@/docs/doc-management';
import { useLeftPanelStore } from '@/features/left-panel/stores/useLeftPanelStore';
import { TreeSkeleton } from '@/features/skeletons/components/TreeSkeleton';
import { useFocusStore } from '@/stores/useFocusStore';
import { useResponsiveStore } from '@/stores/useResponsiveStore';
import { CLASS_DOC_TITLE } from '../../doc-header';
@@ -39,6 +40,8 @@ import { findIndexInTree, isDocNode } from '../utils';
import { DocSubPageItem } from './DocSubPageItem';
import { DocTreeItemActions } from './DocTreeItemActions';
const DOC_TREE_ROOT_ITEM_ID = 'doc-tree-root-item';
type DocTreeProps = {
currentDoc: Doc;
};
@@ -58,6 +61,7 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
const rootItemRef = useRef<HTMLDivElement>(null);
const rootActionsRef = useRef<HTMLDivElement>(null);
const rootButtonOptionRef = useRef<ButtonElement | null>(null);
const { addLastFocus } = useFocusStore();
const { t } = useTranslation();
@@ -102,8 +106,13 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
const handleRootKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const target = e.target as HTMLElement | null;
const isInActions = !!target?.closest('.doc-tree-root-item-actions');
const isOnEmojiButton = !!target?.closest('.--docs--doc-icon');
if (!target || target?.id !== DOC_TREE_ROOT_ITEM_ID) {
return;
}
const isInActions = !!target.closest('.doc-tree-root-item-actions');
const isOnEmojiButton = !!target.closest('.--docs--doc-icon');
const isOnRootItem = target === e.currentTarget;
if (e.key === 'F2' && !rootActionsOpen && !isInActions) {
@@ -142,18 +151,14 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
);
// Handle menu open/close for root item - mirrors DocSubPageItem behavior
const handleRootActionsOpenChange = useCallback((isOpen: boolean) => {
setRootActionsOpen(isOpen);
const handleRootActionsOpenChange = useCallback(
(isOpen: boolean) => {
setRootActionsOpen(isOpen);
// When the menu closes, return focus to the root tree item
// (same behavior as DocSubPageItem for consistency)
// Use requestAnimationFrame for smoother focus transition without flickering
if (!isOpen) {
requestAnimationFrame(() => {
rootItemRef.current?.focus();
});
}
}, []);
addLastFocus(rootItemRef.current);
},
[addLastFocus],
);
/**
* This effect is used to reset the tree when a new document
@@ -325,7 +330,8 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
>
<Box
ref={rootItemRef}
data-testid="doc-tree-root-item"
data-testid={DOC_TREE_ROOT_ITEM_ID}
id={DOC_TREE_ROOT_ITEM_ID}
role="treeitem"
aria-label={`${t('Root document {{title}}', { title: treeContext.root?.title || untitledDocument })}`}
aria-selected={rootIsSelected}
@@ -10,7 +10,7 @@ import {
useTreeContext,
} from '@gouvfr-lasuite/ui-kit';
import { useRouter } from 'next/router';
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -27,6 +27,8 @@ import {
useDocTitleUpdate,
} from '@/docs/doc-management';
import { useDuplicateDoc } from '@/docs/doc-management/api';
import { ConfirmationDuplicateModal } from '@/docs/doc-management/components/ConfirmationDuplicateModal';
import { useFocusStore } from '@/stores/useFocusStore';
import { useDetachDoc } from '../api/useDetach';
import MoveDocIcon from '../assets/doc-extract-bold.svg';
@@ -59,6 +61,8 @@ export const DocTreeItemActions = ({
const router = useRouter();
const { t } = useTranslation();
const deleteModal = useModal();
const { restoreFocus } = useFocusStore();
const [isModalDuplicateOpen, setIsModalDuplicateOpen] = useState(false);
const copyLink = useCopyDocLink(doc.id);
const { mutate: detachDoc } = useDetachDoc();
const treeContext = useTreeContext<Doc | null>();
@@ -141,11 +145,14 @@ export const DocTreeItemActions = ({
icon: <Icon iconName="content_copy" />,
isDisabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
docId: doc.id,
with_accesses: false,
canSave: doc.abilities.partial_update,
});
if (doc.numchild) {
setIsModalDuplicateOpen(true);
} else {
duplicateDoc({
docId: doc.id,
canSave: doc.abilities.partial_update,
});
}
},
},
{
@@ -182,11 +189,16 @@ export const DocTreeItemActions = ({
<Box className="doc-tree-root-item-actions actions">
<Box
ref={targetActionsRef}
className="--docs--doc-tree-item-actions"
$direction="row"
$align="center"
className="--docs--doc-tree-item-actions"
$gap="4xs"
tabIndex={-1}
onKeyDownCapture={(e) => {
if (e.key === 'Escape') {
restoreFocus();
}
}}
$css={css`
& button {
height: 24px;
@@ -248,6 +260,15 @@ export const DocTreeItemActions = ({
</Button>
)}
</Box>
{isModalDuplicateOpen && (
<ConfirmationDuplicateModal
onClose={() => {
setIsModalDuplicateOpen(false);
restoreFocus();
}}
doc={doc}
/>
)}
{deleteModal.isOpen && (
<ModalRemoveDoc
onClose={deleteModal.onClose}
@@ -185,7 +185,7 @@ export const DocMoveModal = ({
<Text as="h2" $margin="0" $size="s" $align="flex-start">
{t('Choose a new parent doc')}
</Text>
<Box $position="absolute" $css="top: 4px; right: 4px;">
<Box $position="absolute" $css="top: 8px; right: 8px;">
<ButtonCloseModal
aria-label={t('Close the move modal')}
onClick={onClose}
@@ -52,6 +52,16 @@ const ConfirmationLeaveModal = dynamic(
{ ssr: false },
);
const ConfirmationDuplicateModal = dynamic(
() =>
import('@/docs/doc-management/components/ConfirmationDuplicateModal').then(
(mod) => ({
default: mod.ConfirmationDuplicateModal,
}),
),
{ ssr: false },
);
interface DocsGridActionsProps {
doc: Doc;
}
@@ -61,6 +71,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
const { restoreFocus, addLastFocus } = useFocusStore();
const [openDropdown, setOpenDropdown] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalDuplicateOpen, setIsModalDuplicateOpen] = useState(false);
const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
const [isModalMoveOpen, setIsModalMoveOpen] = useState(false);
@@ -122,11 +133,14 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
icon: <ContentCopySVG width={24} height={24} aria-hidden="true" />,
isDisabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
docId: doc.id,
with_accesses: false,
canSave: false,
});
if (doc.numchild) {
setIsModalDuplicateOpen(true);
} else {
duplicateDoc({
docId: doc.id,
canSave: false,
});
}
},
showSeparator: true,
},
@@ -204,6 +218,15 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
doc={doc}
/>
)}
{isModalDuplicateOpen && (
<ConfirmationDuplicateModal
onClose={() => {
setIsModalDuplicateOpen(false);
restoreFocus();
}}
doc={doc}
/>
)}
{isModalMoveOpen && (
<DocMoveModal
doc={doc}