diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8949108c6..aefb509a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ and this project adheres to
### Added
+- ✨(frontend) leave a document #2410
- ✨(frontend) add top parent on sub docs search #1952
- ✨(frontend) unauthenticated users can search #2407
diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts
index 02605f7bd..3cef7cd75 100644
--- a/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts
+++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-grid.spec.ts
@@ -4,6 +4,7 @@ import {
clickInEditorShareButton,
createDoc,
getGridRow,
+ getOtherBrowserName,
verifyDocName,
} from './utils-common';
import { addNewMember, connectOtherUserToDoc } from './utils-share';
@@ -166,50 +167,85 @@ test.describe('Document grid item options', () => {
await expect(page.getByText(docTitle)).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',
- user_roles: ['editor'],
- user_role: 'editor',
- },
- ],
- },
- });
- });
+ test('it checks the leave feature', async ({ page, browserName }) => {
+ const [docTitle] = await createDoc(page, `leave doc`, browserName);
+
await page.goto('/');
- const button = page
- .getByTestId(`docs-grid-actions-button-mocked-document-id`)
- .first();
- await expect(button).toBeVisible();
- await button.click();
- const removeButton = page
- .getByTestId(`docs-grid-actions-remove-mocked-document-id`)
- .first();
- await expect(removeButton).toBeVisible();
- await removeButton.isDisabled();
+ // Assert the document is visible in the grid results
+ await expect(
+ page.getByLabel('Documents grid').getByText(docTitle),
+ ).toBeVisible();
+
+ const row = await getGridRow(page, docTitle);
+ await row.getByRole('button', { name: /Open the menu of actions/ }).click();
+
+ await page.getByRole('menuitem', { name: 'Leave' }).click();
+
+ const modal = page.getByRole('dialog', {
+ name: 'Confirmation to leave the document',
+ });
+
+ await expect(
+ modal.getByRole('heading', { name: 'Leave a doc' }),
+ ).toBeVisible();
+
+ // Check the message when the user is the unique owner
+ await expect(
+ modal.getByText(
+ 'You cannot leave this document because you are the unique owner.',
+ ),
+ ).toBeVisible();
+
+ await expect(
+ modal.getByRole('button', {
+ name: 'Confirm leaving the document',
+ }),
+ ).toBeHidden();
+
+ await modal.getByRole('button', { name: 'Close the leave modal' }).click();
+
+ // Assert the document is visible in the search results
+ await page.getByRole('button', { name: 'Search docs' }).click();
+ await page.getByPlaceholder('Type the name of a document').fill(docTitle);
+ await page.getByRole('option').getByText(docTitle).click();
+
+ // We share the doc with another user with owner role
+ const otherBrowserName = getOtherBrowserName(browserName);
+ await page.getByRole('button', { name: 'Share' }).click();
+ await addNewMember(page, 0, 'Owner', otherBrowserName);
+ await expect(
+ page
+ .getByRole('listbox', { name: 'Suggestions' })
+ .getByText(new RegExp(otherBrowserName)),
+ ).toBeVisible();
+ await page.getByRole('button', { name: 'Close the share modal' }).click();
+
+ // Leave the document
+ await page
+ .getByRole('button', { name: /Open the document options/ })
+ .click();
+ await page.getByRole('menuitem', { name: 'Leave' }).click();
+ // Check the message
+ await expect(
+ modal.getByText(
+ 'This document and all the sub-documents will no longer be visible',
+ ),
+ ).toBeVisible();
+ await page
+ .getByRole('button', { name: 'Confirm leaving the document' })
+ .click();
+
+ // We are on the grid page and the document is not visible in the grid results
+ await expect(page.getByLabel('Documents grid')).toBeVisible();
+ await expect(
+ page.getByLabel('Documents grid').getByText(docTitle),
+ ).toBeHidden();
+
+ // We search the document and it's not visible in the search results either
+ await page.getByRole('button', { name: 'Search docs' }).click();
+ await page.getByPlaceholder('Type the name of a document').fill(docTitle);
+ await expect(page.getByRole('option').getByText(docTitle)).toBeHidden();
});
});
diff --git a/src/frontend/apps/impress/src/assets/icons/ui-kit/leave.svg b/src/frontend/apps/impress/src/assets/icons/ui-kit/leave.svg
new file mode 100644
index 000000000..1f3b7860b
--- /dev/null
+++ b/src/frontend/apps/impress/src/assets/icons/ui-kit/leave.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index 9dc412e17..d052dcb10 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -1,4 +1,4 @@
-import { Button, useModal } from '@gouvfr-lasuite/cunningham-react';
+import { Button } from '@gouvfr-lasuite/cunningham-react';
import {
DropdownMenu,
DropdownMenuItem,
@@ -18,6 +18,7 @@ import SharedSVG from '@/assets/icons/ui-kit/group.svg';
import HistorySVG from '@/assets/icons/ui-kit/history.svg';
import KeepSVG from '@/assets/icons/ui-kit/keep.svg';
import KeepOffSVG from '@/assets/icons/ui-kit/keep_off.svg';
+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 {
@@ -60,6 +61,16 @@ const DocShareModal = dynamic(
{ ssr: false },
);
+const ConfirmationLeaveModal = dynamic(
+ () =>
+ import('@/docs/doc-share/components/ConfirmationLeaveModal').then(
+ (mod) => ({
+ default: mod.ConfirmationLeaveModal,
+ }),
+ ),
+ { ssr: false },
+);
+
const ModalExport =
process.env.NEXT_PUBLIC_PUBLISH_AS_MIT === 'false'
? dynamic(
@@ -88,16 +99,18 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const treeContext = useTreeContext();
const router = useRouter();
const { isTopRoot } = useDocUtils(doc);
+ const isTopParent = doc.id === treeContext?.root?.id; // it can be a child but not for the current user
const { authenticated } = useAuth();
const copyCurrentEditorToClipboard = useCopyCurrentEditorToClipboard();
const [openDropdown, setOpenDropdown] = useState(false);
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalExportOpen, setIsModalExportOpen] = useState(false);
- const shareModal = useModal();
+ const [isModalShareOpen, setIsModalShareOpen] = useState(false);
+ const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false);
+ const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
const [isPresenterOpen, setIsPresenterOpen] = useState(false);
- const selectHistoryModal = useModal();
- const { restoreFocus } = useFocusStore();
+ const { restoreFocus, addLastFocus } = useFocusStore();
const { isMobile } = useResponsiveStore();
const copyDocLink = useCopyDocLink(doc.id);
const { mutate: duplicateDoc } = useDuplicateDoc({
@@ -151,7 +164,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
label: t('Share'),
icon: ,
callback: () => {
- shareModal.open();
+ setIsModalShareOpen(true);
},
isHidden: !isTopRoot || !authenticated,
},
@@ -176,7 +189,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
icon: ,
isDisabled: !doc.abilities.versions_list,
callback: () => {
- selectHistoryModal.open();
+ setIsModalHistoryOpen(true);
},
isHidden: isMobile || !doc.abilities.versions_list,
showSeparator: true,
@@ -195,6 +208,19 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
isHidden: !doc.abilities.duplicate,
showSeparator: true,
},
+ {
+ label: t('Leave'),
+ icon: ,
+ callback: () => {
+ setIsModalLeaveOpen(true);
+ },
+ /**
+ * A user can only leave a top parent because we cannot
+ * leave a child if the parent is not left.
+ * ⚠️ This doc can still be a child for other users.
+ */
+ isHidden: !isTopParent,
+ },
{
label: t('Delete'),
icon: ,
@@ -223,6 +249,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
e.stopPropagation();
e.preventDefault();
setOpenDropdown((o) => !o);
+ addLastFocus(e.currentTarget);
}}
/>
@@ -244,7 +271,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}}
doc={doc}
onSuccess={() => {
- const isTopParent = doc.id === treeContext?.root?.id;
const parentId =
treeContext?.treeData.getParentId(doc.id) ||
treeContext?.root?.id;
@@ -261,26 +287,34 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}}
/>
)}
- {selectHistoryModal.isOpen && (
+ {isModalHistoryOpen && (
{
- selectHistoryModal.close();
+ setIsModalHistoryOpen(false);
restoreFocus();
}}
doc={doc}
/>
)}
- {shareModal.isOpen && (
+ {isModalShareOpen && (
{
- shareModal.close();
+ setIsModalShareOpen(false);
+ restoreFocus();
+ }}
+ doc={doc}
+ isRootDoc={isTopParent}
+ />
+ )}
+ {isModalLeaveOpen && (
+ {
+ setIsModalLeaveOpen(false);
restoreFocus();
}}
doc={doc}
- isRootDoc={treeContext?.root?.id === doc.id}
/>
)}
-
{isPresenterOpen && (
-
+
)}
{search && (
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts
new file mode 100644
index 000000000..3e012bf52
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts
@@ -0,0 +1,45 @@
+import {
+ UseMutationOptions,
+ useMutation,
+ useQueryClient,
+} from '@tanstack/react-query';
+
+import { APIError, errorCauses, fetchAPI } from '@/api';
+import { KEY_LIST_DOC } from '@/docs/doc-management/api';
+
+interface LeaveDocProps {
+ docId: string;
+}
+
+export const leaveDoc = async ({ docId }: LeaveDocProps): Promise => {
+ const response = await fetchAPI(`documents/${docId}/leave/`, {
+ method: 'POST',
+ });
+
+ if (!response.ok) {
+ throw new APIError(
+ 'Failed to leave the document',
+ await errorCauses(response),
+ );
+ }
+};
+
+type UseLeaveDocOptions = UseMutationOptions;
+
+export const useLeaveDoc = (options?: UseLeaveDocOptions) => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: leaveDoc,
+ ...options,
+ onSuccess: (data, variables, onMutateResult, context) => {
+ void queryClient.invalidateQueries({
+ queryKey: [KEY_LIST_DOC],
+ });
+
+ if (options?.onSuccess) {
+ void options.onSuccess(data, variables, onMutateResult, context);
+ }
+ },
+ });
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx
new file mode 100644
index 000000000..9052fc938
--- /dev/null
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx
@@ -0,0 +1,257 @@
+import { Button, Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react';
+import { useRouter } from 'next/router';
+import { useMemo } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+import { createGlobalStyle, css } from 'styled-components';
+
+import { Box, ButtonCloseModal, Text } from '@/components';
+import { useAuth } from '@/features/auth';
+
+import { Doc } from '../../doc-management';
+import { useDeleteDocAccess, useDocAccesses } from '../api';
+import { useLeaveDoc } from '../api/useLeaveDoc';
+import { useWhoAmI } from '../hooks/useWhoAmI';
+
+const ModalStyle = createGlobalStyle`
+ .c__modal__footer {
+ margin-top: 0;
+ }
+`;
+
+interface ConfirmationLeaveModalProps {
+ doc: Doc;
+ onClose: () => void;
+}
+
+export const ConfirmationLeaveModal = ({
+ doc,
+ onClose,
+}: ConfirmationLeaveModalProps) => {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const { mutate: leaveDoc, isPending: isLeavePending } = useLeaveDoc({
+ onSuccess: () => {
+ if (router.pathname !== `/`) {
+ void router.push('/');
+ } else {
+ onClose();
+ }
+ },
+ });
+
+ return (
+
+ {doc.abilities.leave ? (
+ leaveDoc({ docId: doc.id })}
+ isPending={isLeavePending}
+ onClose={onClose}
+ />
+ ) : (
+ leaveDoc({ docId: doc.id })}
+ isLeavePending={isLeavePending}
+ onClose={onClose}
+ />
+ )}
+ >
+ }
+ size={ModalSize.MEDIUM}
+ title={
+ <>
+
+ {t('Leave a doc')}
+
+
+
+
+ >
+ }
+ >
+
+
+ {doc.abilities.leave ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
+
+const TextModal = () => {
+ const { t } = useTranslation();
+ return (
+
+ This document and all the sub-documents will no longer be
+ visible in your document list and in your search results. The rights that
+ were given to you on this document will be removed.
+
+ );
+};
+
+const TextModalMember = ({ docId }: { docId: string }) => {
+ const { t } = useTranslation();
+ const { access, isLoading, isError } = useGetMyAccess(docId);
+ const { isLastOwner } = useWhoAmI(access);
+
+ if (isLoading) {
+ return null;
+ }
+
+ if (isError) {
+ return (
+
+ Unable to verify your permissions on this document.{' '}
+ Leaving has been disabled until your access level can
+ be confirmed. Please try again later.
+
+ );
+ }
+
+ if (isLastOwner) {
+ return (
+
+ You cannot leave this document{' '}
+ because you are the unique owner. Add another user with
+ the owner role to ensure you can transfer ownership before leaving the
+ document.
+
+ );
+ }
+
+ return ;
+};
+
+/**
+ * Simple button to leave a doc
+ * The user is not a member of the doc, he can just leave the doc
+ */
+const ButtonsLeaveDoc = ({
+ leave,
+ isPending,
+ onClose,
+}: {
+ leave: () => void;
+ isPending: boolean;
+ onClose: () => void;
+}) => {
+ const { t } = useTranslation();
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+/**
+ * The user is a member of the doc, he can leave the doc but
+ * if he is the last owner, he need to transfer the ownership
+ * before leaving the doc
+ */
+const ButtonsLeaveMemberDoc = ({
+ doc,
+ leave,
+ isLeavePending,
+ onClose,
+}: {
+ doc: Doc;
+ leave: () => void;
+ isLeavePending: boolean;
+ onClose: () => void;
+}) => {
+ const { mutateAsync: deleteDocAccess, isPending: isDeletePending } =
+ useDeleteDocAccess();
+ const { access, isLoading, isError } = useGetMyAccess(doc.id);
+ const { isLastOwner } = useWhoAmI(access);
+
+ /**
+ * If the user is the last owner, or ownership cannot be verified (loading or
+ * error), we don't display the leave button to avoid failing open.
+ */
+ if (isLastOwner || isLoading || isError) {
+ return null;
+ }
+
+ return (
+ {
+ if (access) {
+ await deleteDocAccess({ docId: doc.id, accessId: access.id });
+ leave();
+ } else {
+ leave();
+ }
+ }}
+ isPending={isDeletePending || isLeavePending}
+ onClose={onClose}
+ />
+ );
+};
+
+const useGetMyAccess = (docId: string) => {
+ const { user } = useAuth();
+ const {
+ data: accesses,
+ isLoading,
+ isError,
+ } = useDocAccesses({
+ docId,
+ });
+ const access = useMemo(() => {
+ return accesses?.find((access) => access.user.id === user?.id);
+ }, [accesses, user]);
+
+ return { access, isLoading, isError };
+};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx
index 67e34c9a7..e418987df 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx
@@ -1,9 +1,17 @@
import { Access, Role } from '@/docs/doc-management';
import { useAuth } from '@/features/auth';
-export const useWhoAmI = (access: Access) => {
+export const useWhoAmI = (access?: Access) => {
const { user } = useAuth();
+ if (!access) {
+ return {
+ isLastOwner: false,
+ isOtherOwner: false,
+ isMyself: false,
+ };
+ }
+
const isMyself = user?.id === access.user.id;
const rolesAllowed = access.abilities.set_role_to;
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
index d290a1119..430df4711 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridActions.tsx
@@ -1,7 +1,8 @@
-import { useModal } from '@gouvfr-lasuite/cunningham-react';
+import { Button } from '@gouvfr-lasuite/cunningham-react';
+import { DropdownMenu, DropdownMenuItem } from '@gouvfr-lasuite/ui-kit';
import dynamic from 'next/dynamic';
+import { useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { css } from 'styled-components';
import ContentCopySVG from '@/assets/icons/ui-kit/content_copy.svg';
import DeleteSVG from '@/assets/icons/ui-kit/delete.svg';
@@ -9,8 +10,8 @@ import DocMoveInSVG from '@/assets/icons/ui-kit/doc-move-in.svg';
import GroupSVG from '@/assets/icons/ui-kit/group.svg';
import KeepSVG from '@/assets/icons/ui-kit/keep.svg';
import KeepOffSVG from '@/assets/icons/ui-kit/keep_off.svg';
-import MoreHorizSVG from '@/assets/icons/ui-kit/more_horiz.svg';
-import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
+import LeaveSVG from '@/assets/icons/ui-kit/leave.svg';
+import MoreSVG from '@/assets/icons/ui-kit/more_horiz.svg';
import {
Doc,
KEY_LIST_DOC,
@@ -41,17 +42,28 @@ const ModalRemoveDoc = dynamic(
{ ssr: false },
);
+const ConfirmationLeaveModal = dynamic(
+ () =>
+ import('@/docs/doc-share/components/ConfirmationLeaveModal').then(
+ (mod) => ({
+ default: mod.ConfirmationLeaveModal,
+ }),
+ ),
+ { ssr: false },
+);
+
interface DocsGridActionsProps {
doc: Doc;
}
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
const { t } = useTranslation();
- const restoreFocus = useFocusStore((state) => state.restoreFocus);
-
- const deleteModal = useModal();
- const shareModal = useModal();
- const importModal = useModal();
+ const { restoreFocus, addLastFocus } = useFocusStore();
+ const [openDropdown, setOpenDropdown] = useState(false);
+ const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
+ const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false);
+ const [isModalShareOpen, setIsModalShareOpen] = useState(false);
+ const [isModalMoveOpen, setIsModalMoveOpen] = useState(false);
const { untitledDocument } = useTrans();
const { mutate: duplicateDoc } = useDuplicateDoc({
@@ -69,7 +81,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
listInvalidQueries: [KEY_LIST_DOC, KEY_LIST_FAVORITE_DOC],
});
- const options: DropdownMenuOption[] = [
+ const options: DropdownMenuItem[] = [
{
label: doc.is_favorite ? t('Unpin') : t('Pin'),
icon: doc.is_favorite ? (
@@ -91,7 +103,7 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
label: t('Share'),
icon: ,
callback: () => {
- shareModal.open();
+ setIsModalShareOpen(true);
},
testId: `docs-grid-actions-share-${doc.id}`,
@@ -100,15 +112,15 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
label: t('Move into a doc'),
icon: ,
callback: () => {
- importModal.open();
+ setIsModalMoveOpen(true);
},
- testId: `docs-grid-actions-import-${doc.id}`,
- show: doc.abilities.move,
+ testId: `docs-grid-actions-move-${doc.id}`,
+ isHidden: !doc.abilities.move,
},
{
label: t('Duplicate'),
icon: ,
- disabled: !doc.abilities.duplicate,
+ isDisabled: !doc.abilities.duplicate,
callback: () => {
duplicateDoc({
docId: doc.id,
@@ -118,73 +130,88 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
},
showSeparator: true,
},
+ {
+ label: t('Leave'),
+ icon: ,
+ callback: () => {
+ setIsModalLeaveOpen(true);
+ },
+ },
{
label: t('Delete'),
icon: ,
- callback: () => deleteModal.open(),
- disabled: !doc.abilities.destroy,
+ callback: () => {
+ setIsModalRemoveOpen(true);
+ },
+ isHidden: !doc.abilities.destroy,
testId: `docs-grid-actions-remove-${doc.id}`,
},
];
- const documentTitle = doc.title || untitledDocument;
- const menuLabel = t('Open the menu of actions for the document: {{title}}', {
- title: documentTitle,
- });
-
return (
<>
true}
+ onOpenChange={setOpenDropdown}
>
- }
- $theme="brand"
- $variation="secondary"
- $css={css`
- cursor: pointer;
- &:hover {
- opacity: 0.8;
- }
- `}
- aria-hidden="true"
+ aria-label={t(
+ 'Open the menu of actions for the document: {{title}}',
+ {
+ title: doc.title || untitledDocument,
+ },
+ )}
+ size="small"
+ icon={}
+ color="neutral"
+ variant="tertiary"
+ onClick={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ setOpenDropdown((o) => !o);
+ addLastFocus(e.currentTarget);
+ }}
/>
- {deleteModal.isOpen && (
+ {isModalRemoveOpen && (
{
- deleteModal.onClose();
+ setIsModalRemoveOpen(false);
restoreFocus();
}}
doc={doc}
/>
)}
- {shareModal.isOpen && (
+ {isModalShareOpen && (
{
- shareModal.close();
+ setIsModalShareOpen(false);
restoreFocus();
}}
/>
)}
- {importModal.isOpen && (
+ {isModalLeaveOpen && (
+ {
+ setIsModalLeaveOpen(false);
+ restoreFocus();
+ }}
+ doc={doc}
+ />
+ )}
+ {isModalMoveOpen && (
{
- importModal.close();
+ setIsModalMoveOpen(false);
restoreFocus();
}}
- isOpen={importModal.isOpen}
+ isOpen={isModalMoveOpen}
/>
)}
>
diff --git a/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts b/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts
index 35be6b1d8..e381acddc 100644
--- a/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts
+++ b/src/frontend/apps/impress/src/features/service-worker/plugins/ApiPlugin.ts
@@ -278,6 +278,7 @@ export class ApiPlugin implements WorkboxPlugin {
formatted_content: true,
invite_owner: true,
link_configuration: true,
+ leave: true,
media_auth: true,
move: true,
partial_update: true,
diff --git a/src/frontend/apps/impress/src/i18n/translations.json b/src/frontend/apps/impress/src/i18n/translations.json
index 4ae6de28a..ed7e76ccc 100644
--- a/src/frontend/apps/impress/src/i18n/translations.json
+++ b/src/frontend/apps/impress/src/i18n/translations.json
@@ -1480,6 +1480,8 @@
"The export failed": "L’exportation a échoué",
"The link sharing rules differ from the parent document": "Les règles de partage du lien diffèrent du document parent",
"Thinking...": "Réflexion en cours...",
+ "You cannot leave this document because you are the unique owner. Add another user with the owner role to ensure you can transfer ownership before leaving the document.": "Vous ne pouvez pas quitter ce document parce que vous êtes le propriétaire unique. Ajoutez un autre utilisateur avec le rôle de propriétaire pour vous assurer de pouvoir transférer la propriété avant de quitter le document.",
+ "This document and all the sub-documents will no longer be visible in your document list and in your search results. The rights that were given to you on this document will be removed.": "Ce document et tous les sous-documents ne seront plus visibles dans votre liste de documents et dans vos résultats de recherche. Les droits qui vous ont été accordés sur ce document seront supprimés.",
"This document and any sub-documents will be placed in the trashbin. You can restore it within {{days}} days.": "Ce document et tous les sous-documents seront placés dans la corbeille. Vous pouvez le restaurer dans {{days}} jours.",
"This document will be placed in the trashbin. You can restore it within {{days}} days.": "Ce document sera placé dans la corbeille. Vous pouvez le restaurer sous {{days}} jours.",
"This file is flagged as unsafe.": "Ce fichier est marqué comme non sûr.",