From 0a6cfa3a3305dc977dcc483c61f981e2cc8bbc59 Mon Sep 17 00:00:00 2001 From: Nathan Vasse Date: Thu, 30 Jul 2026 15:49:29 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5(frontend)=20remove=20the=20sharing?= =?UTF-8?q?=20management=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sharing is managed in Drive now: keeping the share modal, invitations and role management in Docs would offer two competing places to do the same thing on the same items. The whole doc-share feature goes away; only read-only visibility badges remain so users still see at a glance that a document is public or shared. The 403 page points users to the document owner instead of an in-app access request, and the shared button in the grid becomes a passive indicator. --- .../doc-header/components/DocFloatingBar.tsx | 2 - .../docs/doc-header/components/DocToolBox.tsx | 66 --- .../doc-management/components/DocPage403.tsx | 52 +-- .../src/features/docs/doc-share/api/index.ts | 10 - .../docs/doc-share/api/useCreateDocAccess.tsx | 71 ---- .../doc-share/api/useCreateDocInvitation.tsx | 53 --- .../docs/doc-share/api/useDeleteDocAccess.ts | 70 --- .../doc-share/api/useDeleteDocInvitation.ts | 70 --- .../doc-share/api/useDocAccessRequest.tsx | 236 ----------- .../docs/doc-share/api/useDocAccesses.tsx | 44 -- .../docs/doc-share/api/useDocInvitations.tsx | 75 ---- .../docs/doc-share/api/useLeaveDoc.ts | 45 -- .../docs/doc-share/api/useUpdateDocAccess.ts | 68 --- .../doc-share/api/useUpdateDocInvitation.ts | 79 ---- .../docs/doc-share/api/useUpdateDocLink.tsx | 60 --- .../features/docs/doc-share/api/useUsers.tsx | 43 -- .../docs/doc-share/assets/desynchro.svg | 15 - .../features/docs/doc-share/assets/undo.svg | 15 - .../components/AlertModalRequestAccess.tsx | 89 ---- .../components/ConfirmationLeaveModal.tsx | 257 ----------- .../components/DocDesynchronized.tsx | 74 ---- .../components/DocInheritedShareContent.tsx | 66 --- .../doc-share/components/DocRoleDropdown.tsx | 156 ------- .../components/DocShareAccessRequest.tsx | 255 ----------- .../components/DocShareAddMemberList.tsx | 171 -------- .../components/DocShareAddMemberListItem.tsx | 55 --- .../doc-share/components/DocShareButton.tsx | 89 ---- .../components/DocShareInvitation.tsx | 187 -------- .../doc-share/components/DocShareMember.tsx | 135 ------ .../doc-share/components/DocShareModal.tsx | 398 ------------------ .../components/DocShareModalFooter.tsx | 50 --- .../doc-share/components/DocVisibility.tsx | 250 ----------- .../doc-share/components/SearchUserRow.tsx | 69 --- .../docs/doc-share/components/index.ts | 3 - .../features/docs/doc-share/hooks/index.ts | 2 - .../hooks/useTranslatedShareSettings.tsx | 52 --- .../docs/doc-share/hooks/useWhoAmI.tsx | 28 -- .../src/features/docs/doc-share/index.ts | 4 - .../src/features/docs/doc-share/types.tsx | 51 --- .../docs-grid/components/DocMoveModal.tsx | 23 - .../docs-grid/components/DocsGridActions.tsx | 56 --- .../components/DocsGridItemSharedButton.tsx | 93 ++-- .../apps/impress/src/pages/docs/new/index.tsx | 46 +- 43 files changed, 50 insertions(+), 3683 deletions(-) delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/index.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocAccess.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocInvitation.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocInvitation.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocInvitation.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/api/useUsers.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/assets/desynchro.svg delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/assets/undo.svg delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/AlertModalRequestAccess.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAccessRequest.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberListItem.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/components/index.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/hooks/index.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/hooks/useTranslatedShareSettings.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/index.ts delete mode 100644 src/frontend/apps/impress/src/features/docs/doc-share/types.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx index 648caefed..b250216d7 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocFloatingBar.tsx @@ -1,7 +1,6 @@ import { Box } from '@/components'; import { CardFloatingBar, FloatingBar } from '@/components/FloatingBar'; import { useDocStore } from '@/docs/doc-management/stores/useDocStore'; -import { DocShareButton } from '@/features/docs/doc-share/components/DocShareButton'; import { RightPanelCollapseButton } from '@/features/right-panel/components/RightPanelCollapseButton'; import { DocLeftPanelCollapseButton } from './DocLeftPanelCollapseButton'; @@ -15,7 +14,6 @@ export const DocFloatingBar = () => { - {!isDeletedDoc && currentDoc && } {!isDeletedDoc && currentDoc && } 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 8ad47dda4..e4eb982f9 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 @@ -14,11 +14,9 @@ import AddLinkSVG from '@/assets/icons/ui-kit/add_link.svg'; import ContentCopySVG from '@/assets/icons/ui-kit/content_copy.svg'; import DeleteSVG from '@/assets/icons/ui-kit/delete.svg'; import DownloadSVG from '@/assets/icons/ui-kit/download.svg'; -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 { @@ -29,11 +27,9 @@ import { useCopyDocLink, useCreateFavoriteDoc, useDeleteFavoriteDoc, - useDocUtils, useDuplicateDoc, } from '@/docs/doc-management'; import { usePresenterStore } from '@/docs/doc-presenter/stores'; -import { useAuth } from '@/features/auth'; import { useFocusStore, useResponsiveStore } from '@/stores'; import { useCopyCurrentEditorToClipboard } from '../hooks/useCopyCurrentEditorToClipboard'; @@ -54,24 +50,6 @@ const ModalSelectVersion = dynamic( { ssr: false }, ); -const DocShareModal = dynamic( - () => - import('@/docs/doc-share/components/DocShareModal').then((mod) => ({ - default: mod.DocShareModal, - })), - { 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( @@ -91,16 +69,12 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { const { t } = useTranslation(); 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 [isModalShareOpen, setIsModalShareOpen] = useState(false); const [isModalHistoryOpen, setIsModalHistoryOpen] = useState(false); - const [isModalLeaveOpen, setIsModalLeaveOpen] = useState(false); const { restoreFocus, addLastFocus } = useFocusStore(); const { isMobile } = useResponsiveStore(); @@ -153,14 +127,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => { icon: diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/index.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/index.ts deleted file mode 100644 index ff9c7b418..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './useCreateDocAccess'; -export * from './useCreateDocInvitation'; -export * from './useDeleteDocAccess'; -export * from './useDeleteDocInvitation'; -export * from './useDocAccesses'; -export * from './useDocAccessRequest'; -export * from './useDocInvitations'; -export * from './useUpdateDocAccess'; -export * from './useUpdateDocInvitation'; -export * from './useUsers'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocAccess.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocAccess.tsx deleted file mode 100644 index 23cd2616b..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocAccess.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { - Access, - Doc, - KEY_DOC, - KEY_LIST_DOC, - Role, -} from '@/docs/doc-management'; -import { User } from '@/features/auth'; -import { useBroadcastStore } from '@/stores/useBroadcastStore'; - -import { OptionType } from '../types'; - -import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses'; -import { KEY_LIST_USER } from './useUsers'; - -interface CreateDocAccessParams { - role: Role; - docId: Doc['id']; - memberId: User['id']; -} - -export const createDocAccess = async ({ - memberId, - role, - docId, -}: CreateDocAccessParams): Promise => { - const response = await fetchAPI(`documents/${docId}/accesses/`, { - method: 'POST', - body: JSON.stringify({ - user_id: memberId, - role, - }), - }); - - if (!response.ok) { - throw new APIError( - `Failed to add the member in the doc.`, - await errorCauses(response, { - type: OptionType.NEW_MEMBER, - }), - ); - } - - return response.json() as Promise; -}; - -export function useCreateDocAccess() { - const queryClient = useQueryClient(); - const { broadcast } = useBroadcastStore(); - - return useMutation({ - mutationFn: createDocAccess, - onSuccess: (_data, variable) => { - void queryClient.resetQueries({ - queryKey: [KEY_LIST_DOC], - }); - void queryClient.resetQueries({ - queryKey: [KEY_LIST_USER], - }); - void queryClient.resetQueries({ - queryKey: [KEY_LIST_DOC_ACCESSES], - }); - - // Broadcast to every user connected to the document - broadcast(`${KEY_DOC}-${variable.docId}`); - }, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocInvitation.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocInvitation.tsx deleted file mode 100644 index a13e42357..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useCreateDocInvitation.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Doc, Role } from '@/docs/doc-management'; -import { User } from '@/features/auth'; - -import { Invitation, OptionType } from '../types'; - -import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations'; - -interface CreateDocInvitationParams { - email: User['email']; - role: Role; - docId: Doc['id']; -} - -export const createDocInvitation = async ({ - email, - role, - docId, -}: CreateDocInvitationParams): Promise => { - const response = await fetchAPI(`documents/${docId}/invitations/`, { - method: 'POST', - body: JSON.stringify({ - email, - role, - }), - }); - - if (!response.ok) { - throw new APIError( - `Failed to create the invitation for ${email}`, - await errorCauses(response, { - value: email, - type: OptionType.INVITATION, - }), - ); - } - - return response.json() as Promise; -}; - -export function useCreateDocInvitation() { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: createDocInvitation, - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_INVITATIONS], - }); - }, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts deleted file mode 100644 index 0acae97a7..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocAccess.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { KEY_DOC, KEY_LIST_DOC } from '@/docs/doc-management'; -import { useBroadcastStore } from '@/stores/useBroadcastStore'; - -import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses'; -import { KEY_LIST_USER } from './useUsers'; - -interface DeleteDocAccessProps { - docId: string; - accessId: string; -} - -export const deleteDocAccess = async ({ - docId, - accessId, -}: DeleteDocAccessProps): Promise => { - const response = await fetchAPI(`documents/${docId}/accesses/${accessId}/`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new APIError( - 'Failed to delete the member', - await errorCauses(response), - ); - } -}; - -type UseDeleteDocAccessOptions = UseMutationOptions< - void, - APIError, - DeleteDocAccessProps ->; - -export const useDeleteDocAccess = (options?: UseDeleteDocAccessOptions) => { - const queryClient = useQueryClient(); - const { broadcast } = useBroadcastStore(); - - return useMutation({ - mutationFn: deleteDocAccess, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_ACCESSES], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_DOC], - }); - - // Broadcast to every user connected to the document - broadcast(`${KEY_DOC}-${variables.docId}`); - - void queryClient.resetQueries({ - queryKey: [KEY_LIST_DOC], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_USER], - }); - if (options?.onSuccess) { - void options.onSuccess(data, variables, onMutateResult, context); - } - }, - }); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocInvitation.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocInvitation.ts deleted file mode 100644 index 3ba722ae9..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDeleteDocInvitation.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; - -import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations'; - -interface DeleteDocInvitationProps { - docId: string; - invitationId: string; -} - -type RemoveDocInvitationError = { - role?: string[]; -}; - -export const deleteDocInvitation = async ({ - docId, - invitationId, -}: DeleteDocInvitationProps): Promise => { - const response = await fetchAPI( - `documents/${docId}/invitations/${invitationId}/`, - { - method: 'DELETE', - }, - ); - - if (!response.ok) { - throw new APIError( - 'Failed to delete the invitation', - await errorCauses(response), - ); - } -}; - -type UseDeleteDocInvitationOptions = UseMutationOptions< - void, - APIError, - DeleteDocInvitationProps ->; - -export const useDeleteDocInvitation = ( - options?: UseDeleteDocInvitationOptions, -) => { - const queryClient = useQueryClient(); - return useMutation< - void, - APIError, - DeleteDocInvitationProps - >({ - mutationFn: deleteDocInvitation, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_INVITATIONS], - }); - if (options?.onSuccess) { - void options.onSuccess(data, variables, onMutateResult, context); - } - }, - onError: (error, variables, onMutateResult, context) => { - if (options?.onError) { - void options.onError(error, variables, onMutateResult, context); - } - }, - }); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx deleted file mode 100644 index 9648a81f8..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccessRequest.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { - UseMutationOptions, - UseQueryOptions, - useMutation, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; - -import { - APIError, - APIList, - errorCauses, - fetchAPI, - useAPIInfiniteQuery, -} from '@/api'; -import { AccessRequest, Doc, Role } from '@/docs/doc-management'; - -import { OptionType } from '../types'; - -import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses'; - -interface CreateDocAccessRequestParams { - docId: Doc['id']; - role?: Role; -} - -export const createDocAccessRequest = async ({ - docId, - role, -}: CreateDocAccessRequestParams): Promise => { - const response = await fetchAPI(`documents/${docId}/ask-for-access/`, { - method: 'POST', - body: JSON.stringify({ - role, - }), - }); - - if (!response.ok) { - throw new APIError( - `Failed to create a request to access to the doc.`, - await errorCauses(response, { - type: OptionType.NEW_MEMBER, - }), - ); - } -}; - -type UseCreateDocAccessRequestOptions = UseMutationOptions< - void, - APIError, - CreateDocAccessRequestParams ->; - -export function useCreateDocAccessRequest( - options?: UseCreateDocAccessRequestOptions, -) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: createDocAccessRequest, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.resetQueries({ - queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS], - }); - - void options?.onSuccess?.(data, variables, onMutateResult, context); - }, - }); -} - -type AccessRequestResponse = APIList; - -interface DocAccessRequestsParams { - docId: Doc['id']; -} - -export type DocAccessRequestsAPIParams = DocAccessRequestsParams & { - page: number; -}; - -export const getDocAccessRequests = async ({ - docId, - page, -}: DocAccessRequestsAPIParams): Promise => { - const response = await fetchAPI( - `documents/${docId}/ask-for-access/?page=${page}`, - ); - - if (!response.ok) { - throw new APIError( - 'Failed to get the doc access requests', - await errorCauses(response), - ); - } - - return response.json() as Promise; -}; - -export const KEY_LIST_DOC_ACCESS_REQUESTS = 'docs-access-requests'; - -export function useDocAccessRequests( - params: DocAccessRequestsAPIParams, - queryConfig?: UseQueryOptions< - AccessRequestResponse, - APIError, - AccessRequestResponse - >, -) { - return useQuery({ - queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS, params], - queryFn: () => getDocAccessRequests(params), - ...queryConfig, - }); -} - -export const useDocAccessRequestsInfinite = ( - params: DocAccessRequestsParams, -) => { - return useAPIInfiniteQuery( - KEY_LIST_DOC_ACCESS_REQUESTS, - getDocAccessRequests, - params, - ); -}; - -interface acceptDocAccessRequestsParams { - docId: string; - accessRequestId: string; - role: Role; -} - -export const acceptDocAccessRequests = async ({ - docId, - accessRequestId, - role, -}: acceptDocAccessRequestsParams): Promise => { - const response = await fetchAPI( - `documents/${docId}/ask-for-access/${accessRequestId}/accept/`, - { - method: 'POST', - body: JSON.stringify({ - role, - }), - }, - ); - - if (!response.ok) { - throw new APIError( - 'Failed to accept the access request', - await errorCauses(response), - ); - } -}; - -type UseAcceptDocAccessRequests = Partial; - -type UseAcceptDocAccessRequestsOptions = UseMutationOptions< - void, - APIError, - UseAcceptDocAccessRequests ->; - -export const useAcceptDocAccessRequest = ( - options?: UseAcceptDocAccessRequestsOptions, -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: acceptDocAccessRequests, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_ACCESSES], - }); - - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS], - }); - - if (options?.onSuccess) { - void options.onSuccess(data, variables, onMutateResult, context); - } - }, - }); -}; - -interface DeleteDocAccessRequestParams { - docId: string; - accessRequestId: string; -} - -export const deleteDocAccessRequest = async ({ - docId, - accessRequestId, -}: DeleteDocAccessRequestParams): Promise => { - const response = await fetchAPI( - `documents/${docId}/ask-for-access/${accessRequestId}/`, - { - method: 'DELETE', - }, - ); - - if (!response.ok) { - throw new APIError( - 'Failed to delete the access request', - await errorCauses(response), - ); - } -}; - -type UseDeleteDocAccessRequestOptions = UseMutationOptions< - void, - APIError, - DeleteDocAccessRequestParams ->; - -export const useDeleteDocAccessRequest = ( - options?: UseDeleteDocAccessRequestOptions, -) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: deleteDocAccessRequest, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_ACCESS_REQUESTS], - }); - - if (options?.onSuccess) { - void options.onSuccess(data, variables, onMutateResult, context); - } - }, - }); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx deleted file mode 100644 index 87b7d5545..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocAccesses.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Access } from '@/docs/doc-management'; - -export type DocAccessesParams = { - docId: string; - ordering?: string; -}; - -export const getDocAccesses = async ({ - docId, - ordering, -}: DocAccessesParams): Promise => { - let url = `documents/${docId}/accesses/`; - - if (ordering) { - url += '&ordering=' + ordering; - } - - const response = await fetchAPI(url); - - if (!response.ok) { - throw new APIError( - 'Failed to get the doc accesses', - await errorCauses(response), - ); - } - - return (await response.json()) as Access[]; -}; - -export const KEY_LIST_DOC_ACCESSES = 'docs-accesses'; - -export function useDocAccesses( - params: DocAccessesParams, - queryConfig?: UseQueryOptions, -) { - return useQuery({ - queryKey: [KEY_LIST_DOC_ACCESSES, params], - queryFn: () => getDocAccesses(params), - ...queryConfig, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx deleted file mode 100644 index dbea614b9..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useDocInvitations.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; - -import { - APIError, - APIList, - errorCauses, - fetchAPI, - useAPIInfiniteQuery, -} from '@/api'; - -import { Invitation } from '../types'; - -export type DocInvitationsParams = { - docId: string; - ordering?: string; -}; - -export type DocInvitationsAPIParams = DocInvitationsParams & { - page: number; -}; - -type DocInvitationsResponse = APIList; - -export const getDocInvitations = async ({ - page, - docId, - ordering, -}: DocInvitationsAPIParams): Promise => { - let url = `documents/${docId}/invitations/?page=${page}`; - - if (ordering) { - url += '&ordering=' + ordering; - } - - const response = await fetchAPI(url); - - if (!response.ok) { - throw new APIError( - 'Failed to get the doc accesses', - await errorCauses(response), - ); - } - - return response.json() as Promise; -}; - -export const KEY_LIST_DOC_INVITATIONS = 'docs-invitations'; - -export function useDocInvitations( - params: DocInvitationsAPIParams, - queryConfig?: UseQueryOptions< - DocInvitationsResponse, - APIError, - DocInvitationsResponse - >, -) { - return useQuery({ - queryKey: [KEY_LIST_DOC_INVITATIONS, params], - queryFn: () => getDocInvitations(params), - ...queryConfig, - }); -} - -/** - * @param param Used for infinite scroll pagination - * @param queryConfig - * @returns - */ -export function useDocInvitationsInfinite(params: DocInvitationsParams) { - return useAPIInfiniteQuery( - KEY_LIST_DOC_INVITATIONS, - getDocInvitations, - params, - ); -} 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 deleted file mode 100644 index 3e012bf52..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useLeaveDoc.ts +++ /dev/null @@ -1,45 +0,0 @@ -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/api/useUpdateDocAccess.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts deleted file mode 100644 index 3f54b7fe6..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocAccess.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Access, KEY_DOC, KEY_LIST_DOC, Role } from '@/docs/doc-management'; - -import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses'; - -interface UpdateDocAccessProps { - docId: string; - accessId: string; - role: Role; -} - -export const updateDocAccess = async ({ - docId, - accessId, - role, -}: UpdateDocAccessProps): Promise => { - const response = await fetchAPI(`documents/${docId}/accesses/${accessId}/`, { - method: 'PATCH', - body: JSON.stringify({ - role, - }), - }); - - if (!response.ok) { - throw new APIError('Failed to update role', await errorCauses(response)); - } - - return response.json() as Promise; -}; - -type UseUpdateDocAccess = Partial; - -type UseUpdateDocAccessOptions = UseMutationOptions< - Access, - APIError, - UseUpdateDocAccess ->; - -export const useUpdateDocAccess = (options?: UseUpdateDocAccessOptions) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: updateDocAccess, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_ACCESSES], - }); - void queryClient.invalidateQueries({ - queryKey: [KEY_DOC], - }); - - 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/api/useUpdateDocInvitation.ts b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocInvitation.ts deleted file mode 100644 index a7170ec0e..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocInvitation.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Role } from '@/docs/doc-management'; - -import { Invitation } from '../types'; - -import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations'; - -interface UpdateDocInvitationProps { - docId: string; - invitationId: string; - role: Role; -} - -type UpdateDocInvitationError = { - role?: string[]; -}; - -export const updateDocInvitation = async ({ - docId, - invitationId, - role, -}: UpdateDocInvitationProps): Promise => { - const response = await fetchAPI( - `documents/${docId}/invitations/${invitationId}/`, - { - method: 'PATCH', - body: JSON.stringify({ - role, - }), - }, - ); - - if (!response.ok) { - throw new APIError('Failed to update role', await errorCauses(response)); - } - - return response.json() as Promise; -}; - -type UseUpdateDocInvitation = Partial; - -type UseUpdateDocInvitationOptions = UseMutationOptions< - Invitation, - APIError, - UseUpdateDocInvitation ->; - -export const useUpdateDocInvitation = ( - options?: UseUpdateDocInvitationOptions, -) => { - const queryClient = useQueryClient(); - return useMutation< - Invitation, - APIError, - UpdateDocInvitationProps - >({ - mutationFn: updateDocInvitation, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - void queryClient.invalidateQueries({ - queryKey: [KEY_LIST_DOC_INVITATIONS], - }); - if (options?.onSuccess) { - void options.onSuccess(data, variables, onMutateResult, context); - } - }, - onError: (error, variables, onMutateResult, context) => { - if (options?.onError) { - void options.onError(error, variables, onMutateResult, context); - } - }, - }); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx deleted file mode 100644 index 5a950d0e8..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUpdateDocLink.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { - UseMutationOptions, - useMutation, - useQueryClient, -} from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Doc, LinkReach, LinkRole } from '@/docs/doc-management'; - -export type UpdateDocLinkParams = Pick & - Partial>; - -type UpdateDocLinkResponse = { link_role: LinkRole; link_reach: LinkReach }; - -export const updateDocLink = async ({ - id, - ...params -}: UpdateDocLinkParams): Promise => { - const response = await fetchAPI(`documents/${id}/link-configuration/`, { - method: 'PUT', - body: JSON.stringify({ - ...params, - }), - }); - - if (!response.ok) { - throw new APIError( - 'Failed to update the doc link', - await errorCauses(response), - ); - } - - return response.json() as Promise; -}; - -type UseUpdateDocLinkOptions = UseMutationOptions< - UpdateDocLinkResponse, - APIError, - UpdateDocLinkParams -> & { - listInvalidQueries?: string[]; -}; - -export function useUpdateDocLink(options?: UseUpdateDocLinkOptions) { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: updateDocLink, - ...options, - onSuccess: (data, variables, onMutateResult, context) => { - options?.listInvalidQueries?.forEach((queryKey) => { - void queryClient.invalidateQueries({ - queryKey: [queryKey], - }); - }); - - options?.onSuccess?.(data, variables, onMutateResult, context); - }, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUsers.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/api/useUsers.tsx deleted file mode 100644 index ed029222e..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/api/useUsers.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { UseQueryOptions, useQuery } from '@tanstack/react-query'; - -import { APIError, errorCauses, fetchAPI } from '@/api'; -import { Doc } from '@/docs/doc-management'; -import { User } from '@/features/auth'; - -export type UsersParams = { - query: string; - docId: Doc['id']; -}; - -type UsersResponse = User[]; - -export const getUsers = async ({ - query, - docId, -}: UsersParams): Promise => { - const queriesParams = []; - queriesParams.push(query ? `q=${encodeURIComponent(query)}` : ''); - queriesParams.push(docId ? `document_id=${docId}` : ''); - const queryParams = queriesParams.filter(Boolean).join('&'); - - const response = await fetchAPI(`users/?${queryParams}`); - - if (!response.ok) { - throw new APIError('Failed to get the users', await errorCauses(response)); - } - - return response.json() as Promise; -}; - -export const KEY_LIST_USER = 'users'; - -export function useUsers( - param: UsersParams, - queryConfig?: UseQueryOptions, -) { - return useQuery({ - queryKey: [KEY_LIST_USER, param], - queryFn: () => getUsers(param), - ...queryConfig, - }); -} diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/assets/desynchro.svg b/src/frontend/apps/impress/src/features/docs/doc-share/assets/desynchro.svg deleted file mode 100644 index d682fe52b..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/assets/desynchro.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/assets/undo.svg b/src/frontend/apps/impress/src/features/docs/doc-share/assets/undo.svg deleted file mode 100644 index 139ce42dd..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/assets/undo.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/AlertModalRequestAccess.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/AlertModalRequestAccess.tsx deleted file mode 100644 index 6060b0a6b..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/AlertModalRequestAccess.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { Trans, useTranslation } from 'react-i18next'; - -import { AlertModal, Box, Icon, Text } from '@/components'; - -import { useDocAccessRequests } from '../api/useDocAccessRequest'; - -import { ButtonAccessRequest } from './DocShareAccessRequest'; - -interface AlertModalRequestAccessProps { - docId: string; - isOpen: boolean; - onClose: () => void; - onConfirm: () => void; - targetDocumentTitle: string; - title: string; -} - -export const AlertModalRequestAccess = ({ - docId, - isOpen, - onClose, - onConfirm, - targetDocumentTitle, - title, -}: AlertModalRequestAccessProps) => { - const { t } = useTranslation(); - const { data: requests } = useDocAccessRequests({ - docId, - page: 1, - }); - - const hasRequested = !!( - requests && requests?.results.find((request) => request.document === docId) - ); - - return ( - - - }} - /> - - {hasRequested && ( - - - {t('You have already requested access to this document.')} - - )} - - } - confirmLabel={t('Request access')} - onConfirm={onConfirm} - rightActions={ - - - - - } - /> - ); -}; 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 deleted file mode 100644 index 9052fc938..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/ConfirmationLeaveModal.tsx +++ /dev/null @@ -1,257 +0,0 @@ -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/components/DocDesynchronized.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx deleted file mode 100644 index a92480c09..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocDesynchronized.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - Button, - VariantType, - useToastProvider, -} from '@gouvfr-lasuite/cunningham-react'; -import { useTranslation } from 'react-i18next'; - -import { Box, Card, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { Doc, KEY_DOC, KEY_LIST_DOC } from '@/docs/doc-management'; - -import { useUpdateDocLink } from '../api/useUpdateDocLink'; - -import Desync from './../assets/desynchro.svg'; -import Undo from './../assets/undo.svg'; - -interface DocDesynchronizedProps { - doc: Doc; -} - -export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - const { toast } = useToastProvider(); - - const { mutate: updateDocLink } = useUpdateDocLink({ - listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], - onSuccess: () => { - toast(t('The document visibility restored.'), VariantType.SUCCESS, { - duration: 2000, - }); - }, - }); - - return ( - - - - - {t('The link sharing rules differ from the parent document')} - - - {doc.abilities.accesses_manage && ( - - )} - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx deleted file mode 100644 index 0e0b98fd7..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { Fragment } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box, HorizontalSeparator, Icon, StyledLink, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; - -import { Access, useDocStore } from '../../doc-management'; - -import { DocShareMemberItem } from './DocShareMember'; - -type DocInheritedShareContentProps = { - rawAccesses: Access[]; -}; - -export const DocInheritedShareContent = ({ - rawAccesses, -}: DocInheritedShareContentProps) => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - const { currentDoc } = useDocStore(); - - // Check if accesses map is empty - const hasAccesses = rawAccesses.length > 0; - - if (!hasAccesses) { - return null; - } - - return ( - - - - - - {t('People with access via the parent document')} - - - - - - {doc.abilities.accesses_manage && ( - - removeDocAccess({ - accessRequestId: accessRequest.id, - docId: doc.id, - }) - } - aria-label={t('Close the access request modal')} - > - - - )} - - } - /> - - ); -}; - -interface QuickSearchGroupAccessRequestProps { - doc: Doc; -} - -export const QuickSearchGroupAccessRequest = ({ - doc, -}: QuickSearchGroupAccessRequestProps) => { - const { t } = useTranslation(); - const accessRequestQuery = useDocAccessRequestsInfinite({ docId: doc.id }); - - const accessRequestsData: QuickSearchData = useMemo(() => { - const accessRequests = - accessRequestQuery.data?.pages.flatMap((page) => page.results) || []; - - return { - groupName: t('Access Requests'), - elements: accessRequests, - endActions: accessRequestQuery.hasNextPage - ? [ - { - content: , - onSelect: () => void accessRequestQuery.fetchNextPage(), - }, - ] - : undefined, - }; - }, [accessRequestQuery, t]); - - if (!accessRequestsData.elements.length) { - return null; - } - - return ( - <> - - - ( - - )} - /> - - - - ); -}; - -type ButtonAccessRequestProps = { - docId: Doc['id']; -} & Omit & { - onClick?: MouseEventHandler; - }; - -export const ButtonAccessRequest = ({ - docId, - onClick, - ...buttonProps -}: ButtonAccessRequestProps) => { - const { authenticated } = useAuth(); - const { - data: requests, - error: docAccessError, - isLoading, - } = useDocAccessRequests({ - docId, - page: 1, - }); - const { t } = useTranslation(); - const { toast } = useToastProvider(); - const { mutate: createRequest } = useCreateDocAccessRequest({ - onSuccess: () => { - toast(t('Access request sent successfully.'), VariantType.SUCCESS, { - duration: 3000, - }); - }, - }); - - if (!authenticated) { - return null; - } - - if (docAccessError?.status === 404) { - return ( - - {t( - 'As this is a sub-document, please request access to the parent document to enable these features.', - )} - - ); - } - - if (isLoading) { - return ; - } - - const hasRequested = !!( - requests && requests?.results.find((request) => request.document === docId) - ); - - return ( - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx deleted file mode 100644 index 0db52f351..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { - Button, - VariantType, - useToastProvider, -} from '@gouvfr-lasuite/cunningham-react'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { APIError } from '@/api'; -import { Box, Card } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { Doc, Role } from '@/docs/doc-management'; -import { User } from '@/features/auth'; -import { useResponsiveStore } from '@/stores'; - -import { useCreateDocAccess, useCreateDocInvitation } from '../api'; -import { OptionType } from '../types'; - -import { DocRoleDropdown } from './DocRoleDropdown'; -import { DocShareAddMemberListItem } from './DocShareAddMemberListItem'; - -type APIErrorUser = APIError<{ - value: string; - type: OptionType; -}>; - -type Props = { - doc: Doc; - selectedUsers: User[]; - onRemoveUser?: (user: User) => void; - onSubmit?: (selectedUsers: User[], role: Role) => void; - afterInvite?: () => void; -}; -export const DocShareAddMemberList = ({ - doc, - selectedUsers, - onRemoveUser, - afterInvite, -}: Props) => { - const { t } = useTranslation(); - const { toast } = useToastProvider(); - const { isSmallMobile } = useResponsiveStore(); - const [isLoading, setIsLoading] = useState(false); - const { spacingsTokens } = useCunninghamTheme(); - const [invitationRole, setInvitationRole] = useState(Role.EDITOR); - const canShare = doc.abilities.accesses_manage; - const { mutateAsync: createInvitation } = useCreateDocInvitation(); - const { mutateAsync: createDocAccess } = useCreateDocAccess(); - - const onError = (dataError: APIErrorUser) => { - let messageError = - dataError['data']?.type === OptionType.INVITATION - ? t(`Failed to create the invitation for {{email}}.`, { - email: dataError['data']?.value, - }) - : t(`Failed to add the member in the document.`); - - if ( - dataError.cause?.[0] === - 'Document invitation with this Email address and Document already exists.' - ) { - messageError = t('"{{email}}" is already invited to the document.', { - email: dataError['data']?.value, - }); - } - - if ( - dataError.cause?.[0] === - 'This email is already associated to a registered user.' - ) { - messageError = t('"{{email}}" is already member of the document.', { - email: dataError['data']?.value, - }); - } - - toast(messageError, VariantType.ERROR, { - duration: 4000, - }); - }; - - const onInvite = async () => { - setIsLoading(true); - const promises = selectedUsers.map((user) => { - const isInvitationMode = user.id === user.email; - - const payload = { - role: invitationRole, - docId: doc.id, - }; - - return isInvitationMode - ? createInvitation({ - ...payload, - email: user.email.toLowerCase(), - }) - : createDocAccess({ - ...payload, - memberId: user.id, - }); - }); - - const settledPromises = await Promise.allSettled(promises); - settledPromises.forEach((settledPromise) => { - if (settledPromise.status === 'rejected') { - onError(settledPromise.reason as APIErrorUser); - } - }); - afterInvite?.(); - setIsLoading(false); - }; - const inviteLabel = - selectedUsers.length === 1 - ? t('Invite {{name}}', { - name: selectedUsers[0].full_name || selectedUsers[0].email, - }) - : t('Invite {{count}} members', { count: selectedUsers.length }); - - return ( - - - {selectedUsers.map((user) => ( - - ))} - - - - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberListItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberListItem.tsx deleted file mode 100644 index a973acd73..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberListItem.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; - -import { Box, BoxButton, Icon, Text } from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { User } from '@/features/auth'; - -type Props = { - user: User; - onRemoveUser?: (user: User) => void; -}; -export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - - return ( - - - {user.full_name || user.email} - - onRemoveUser?.(user)} - aria-label={t('Remove {{name}} from the invite list', { - name: user.full_name || user.email, - })} - $withThemeInherited - > - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx deleted file mode 100644 index bdba2a761..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareButton.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Button, useModal } from '@gouvfr-lasuite/cunningham-react'; -import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; -import dynamic from 'next/dynamic'; -import { useTranslation } from 'react-i18next'; - -import SharedSVG from '@/assets/icons/ui-kit/shared.svg'; -import { CardFloatingBar } from '@/components/FloatingBar'; -import { Doc } from '@/docs/doc-management/types'; -import { useAuth } from '@/features/auth'; -import { useFocusStore } from '@/stores/useFocusStore'; - -import { KEY_LIST_DOC_ACCESSES, useDocAccesses } from '../api'; - -const DocShareModal = dynamic( - () => - import('./DocShareModal').then((mod) => ({ - default: mod.DocShareModal, - })), - { ssr: false }, -); - -interface DocShareButtonProps { - doc: Doc; - isDisabled?: boolean; - isHidden?: boolean; -} - -export const DocShareButton = ({ - doc, - isDisabled, - isHidden, -}: DocShareButtonProps) => { - const { t } = useTranslation(); - const { addLastFocus, restoreFocus } = useFocusStore(); - const treeContext = useTreeContext(); - const modalShare = useModal(); - const { data: accesses } = useDocAccesses( - { - docId: doc.id, - }, - { - enabled: doc.abilities.accesses_view, - queryKey: [KEY_LIST_DOC_ACCESSES, doc.id], - }, - ); - const { authenticated } = useAuth(); - - const hasAccesses = !!accesses && accesses.length > 1; // more than the current user - - if (isHidden || !authenticated) { - return null; - } - - return ( - <> - - - - {modalShare.isOpen && ( - { - modalShare.close(); - restoreFocus(); - }} - doc={doc} - isRootDoc={treeContext?.root?.id === doc.id} - /> - )} - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx deleted file mode 100644 index d7f72df12..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { - VariantType, - useToastProvider, -} from '@gouvfr-lasuite/cunningham-react'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; - -import { - Box, - BoxButton, - HorizontalSeparator, - Icon, - LoadMoreText, - Text, -} from '@/components'; -import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search'; -import { useCunninghamTheme } from '@/cunningham'; -import { Doc, Role } from '@/docs/doc-management'; -import { User } from '@/features/auth'; - -import { useDocInvitationsInfinite, useUpdateDocInvitation } from '../api'; -import { Invitation } from '../types'; - -import { DocRoleDropdown } from './DocRoleDropdown'; -import { SearchUserRow } from './SearchUserRow'; - -type DocShareInvitationItemProps = { - doc: Doc; - invitation: Invitation; -}; - -export const DocShareInvitationItem = ({ - doc, - invitation, -}: DocShareInvitationItemProps) => { - const { t } = useTranslation(); - const { spacingsTokens } = useCunninghamTheme(); - const invitedUser: User = { - id: invitation.email, - full_name: invitation.email, - email: invitation.email, - short_name: invitation.email, - language: 'en-us', - is_first_connection: false, - }; - - const { toast } = useToastProvider(); - const canUpdate = doc.abilities.accesses_manage; - - const { mutate: updateDocInvitation } = useUpdateDocInvitation({ - onError: (error) => { - toast( - error?.data?.role?.[0] ?? t('Error during update invitation'), - VariantType.ERROR, - { - duration: 4000, - }, - ); - }, - }); - - const onUpdate = (newRole: Role) => { - updateDocInvitation({ - docId: doc.id, - role: newRole, - invitationId: invitation.id, - }); - }; - - return ( - - - - - } - /> - - ); -}; - -type DocShareModalInviteUserRowProps = { - user: User; -}; -export const DocShareModalInviteUserRow = ({ - user, -}: DocShareModalInviteUserRowProps) => { - const { t } = useTranslation(); - return ( - - - - {t('Add')} - - - - } - /> - - ); -}; - -interface QuickSearchGroupInvitationProps { - doc: Doc; -} - -export const QuickSearchGroupInvitation = ({ - doc, -}: QuickSearchGroupInvitationProps) => { - const { t } = useTranslation(); - const { data, hasNextPage, fetchNextPage } = useDocInvitationsInfinite({ - docId: doc.id, - }); - - const invitationsData: QuickSearchData = useMemo(() => { - const invitations = data?.pages.flatMap((page) => page.results) || []; - - return { - groupName: t('Pending invitations'), - elements: invitations, - endActions: hasNextPage - ? [ - { - content: , - onSelect: () => void fetchNextPage(), - }, - ] - : undefined, - }; - }, [data?.pages, fetchNextPage, hasNextPage, t]); - - if (!invitationsData.elements.length) { - return null; - } - - return ( - <> - - ( - - )} - /> - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx deleted file mode 100644 index bb8ccbb39..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { - VariantType, - useToastProvider, -} from '@gouvfr-lasuite/cunningham-react'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { Box } from '@/components'; -import { QuickSearchData } from '@/components/quick-search'; -import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup'; -import { useCunninghamTheme } from '@/cunningham'; -import { Access, Doc, Role } from '@/docs/doc-management/'; - -import { useDocAccesses, useUpdateDocAccess } from '../api'; -import { useWhoAmI } from '../hooks/'; - -import { DocRoleDropdown } from './DocRoleDropdown'; -import { SearchUserRow } from './SearchUserRow'; - -type Props = { - doc?: Doc; - access: Access; - isInherited?: boolean; -}; -export const DocShareMemberItem = ({ - doc, - access, - isInherited = false, -}: Props) => { - const { t } = useTranslation(); - const { isLastOwner } = useWhoAmI(access); - const { toast } = useToastProvider(); - const { spacingsTokens } = useCunninghamTheme(); - - const message = isLastOwner - ? t( - 'You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.', - ) - : undefined; - - const { mutate: updateDocAccess } = useUpdateDocAccess({ - onError: () => { - toast(t('Error while updating the member role.'), VariantType.ERROR, { - duration: 4000, - }); - }, - }); - - const onUpdate = (newRole: Role) => { - if (!doc) { - return; - } - updateDocAccess({ - docId: doc.id, - role: newRole, - accessId: access.id, - }); - }; - - const canUpdate = isInherited ? false : !!doc?.abilities.accesses_manage; - - return ( - - - - - } - /> - - ); -}; - -interface QuickSearchGroupMemberProps { - doc: Doc; -} - -export const QuickSearchGroupMember = ({ - doc, -}: QuickSearchGroupMemberProps) => { - const { t } = useTranslation(); - const membersQuery = useDocAccesses({ - docId: doc.id, - }); - - const membersData: QuickSearchData = useMemo(() => { - const members = membersQuery.data || []; - - const count = members.length; - - return { - groupName: - count === 1 - ? t('Document owner') - : t('Share with {{count}} users', { - count: count, - }), - elements: members, - endActions: undefined, - }; - }, [membersQuery.data, t]); - - return ( - - ( - - )} - /> - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx deleted file mode 100644 index 01f1c729a..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx +++ /dev/null @@ -1,398 +0,0 @@ -import { Modal, ModalSize } from '@gouvfr-lasuite/cunningham-react'; -import { announce } from '@react-aria/live-announcer'; -import { useQueryClient } from '@tanstack/react-query'; -import { useEffect, useMemo, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { createGlobalStyle, css } from 'styled-components'; -import { useDebouncedCallback } from 'use-debounce'; - -import { Box, ButtonCloseModal, HorizontalSeparator, Text } from '@/components'; -import { - QuickSearch, - QuickSearchData, - QuickSearchGroup, -} from '@/components/quick-search/'; -import { useConfig } from '@/core'; -import { Doc } from '@/docs/doc-management'; -import { User } from '@/features/auth'; -import { useResponsiveStore } from '@/stores'; -import { isValidEmail } from '@/utils'; - -import { - KEY_LIST_DOC_ACCESSES, - KEY_LIST_DOC_ACCESS_REQUESTS, - KEY_LIST_DOC_INVITATIONS, - KEY_LIST_USER, - useDocAccesses, - useUsers, -} from '../api'; - -import { DocInheritedShareContent } from './DocInheritedShareContent'; -import { - ButtonAccessRequest, - QuickSearchGroupAccessRequest, -} from './DocShareAccessRequest'; -import { DocShareAddMemberList } from './DocShareAddMemberList'; -import { - DocShareModalInviteUserRow, - QuickSearchGroupInvitation, -} from './DocShareInvitation'; -import { QuickSearchGroupMember } from './DocShareMember'; -import { DocShareModalFooter } from './DocShareModalFooter'; - -const ShareModalStyle = createGlobalStyle` - .--docs--doc-share-modal [cmdk-item] { - cursor: auto; - } - .c__modal__title { - padding-bottom: 0 !important; - } -`; - -type Props = { - doc: Doc; - isRootDoc?: boolean; - onClose: () => void; -}; - -export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => { - const { t } = useTranslation(); - const selectedUsersRef = useRef(null); - const queryClient = useQueryClient(); - const { data: config } = useConfig(); - const API_USERS_SEARCH_QUERY_MIN_LENGTH = - config?.API_USERS_SEARCH_QUERY_MIN_LENGTH || 5; - - const { isLargeScreen } = useResponsiveStore(); - - /** - * The modal content height is calculated based on the viewport height. - * The formula is: - * 100dvh - 2em - 12px - 34px - * - 34px is the height of the modal title in mobile - * - 2em is the padding of the modal content - * - 12px is the padding of the modal footer - * - 690px is the height of the content in desktop - * This ensures that the modal content is always visible and does not overflow. - */ - const modalContentHeight = isLargeScreen - ? 'min(690px, calc(100dvh - 2em - 12px - 34px))' - : `calc(100dvh - 34px)`; - const [selectedUsers, setSelectedUsers] = useState([]); - const [userQuery, setUserQuery] = useState(''); - const [inputValue, setInputValue] = useState(''); - - const [listHeight, setListHeight] = useState('400px'); - const canShare = doc.abilities.accesses_manage && isRootDoc; - const canViewAccesses = doc.abilities.accesses_view; - const showMemberSection = inputValue === '' && selectedUsers.length === 0; - const showFooter = selectedUsers.length === 0 && !inputValue; - - const onSelect = (user: User) => { - setSelectedUsers((prev) => [...prev, user]); - setUserQuery(''); - setInputValue(''); - - const userName = user.full_name || user.email; - announce( - t( - '{{name}} added to invite list. Add more members or press Tab to select role and invite.', - { - name: userName, - }, - ), - 'polite', - ); - }; - - const { data: membersQuery } = useDocAccesses({ - docId: doc.id, - }); - - const searchUsersQuery = useUsers( - { query: userQuery, docId: doc.id }, - { - enabled: userQuery?.length >= API_USERS_SEARCH_QUERY_MIN_LENGTH, - queryKey: [KEY_LIST_USER, { query: userQuery }], - }, - ); - - const onFilter = useDebouncedCallback((str: string) => { - setUserQuery(str); - }, 300); - - const onRemoveUser = (row: User) => { - setSelectedUsers((prevState) => { - const index = prevState.findIndex((value) => value.id === row.id); - if (index < 0) { - return prevState; - } - const newArray = [...prevState]; - newArray.splice(index, 1); - - const userName = row.full_name || row.email; - announce( - t('{{name}} removed from invite list', { - name: userName, - }), - 'polite', - ); - - return newArray; - }); - }; - - const handleRef = (node: HTMLDivElement) => { - const inputHeight = canShare ? 70 : 0; - const marginTop = 11; - const footerHeight = node?.clientHeight ?? 0; - const selectedUsersHeight = selectedUsersRef.current?.clientHeight ?? 0; - const height = `calc(${modalContentHeight} - ${footerHeight}px - ${selectedUsersHeight}px - ${inputHeight}px - ${marginTop}px)`; - - setListHeight(height); - }; - - const inheritedAccesses = useMemo(() => { - return ( - membersQuery?.filter((access) => access.document.id !== doc.id) ?? [] - ); - }, [membersQuery, doc.id]); - - const showInheritedShareContent = - inheritedAccesses.length > 0 && showMemberSection && !isRootDoc; - - // Invalidate relevant queries to ensure fresh data on modal open - useEffect(() => { - [ - KEY_LIST_DOC_INVITATIONS, - KEY_LIST_DOC_ACCESS_REQUESTS, - KEY_LIST_DOC_ACCESSES, - ].forEach((key) => { - void queryClient.invalidateQueries({ - queryKey: [key], - }); - }); - }, [queryClient]); - - return ( - <> - - - {t('Share the document')} - - - - } - hideCloseButton - > - - - - - {canShare && selectedUsers.length > 0 && ( - - { - setUserQuery(''); - setInputValue(''); - setSelectedUsers([]); - }} - /> - - )} - {!canViewAccesses && ( - - )} - - - - {!canViewAccesses && ( - - - {t( - 'You can view this document but need additional access to see its members or modify settings.', - )} - - - - )} - {canViewAccesses && ( - { - setInputValue(str); - onFilter(str); - }} - inputValue={inputValue} - showInput={canShare} - loading={searchUsersQuery.isLoading} - placeholder={t('Type a name or email')} - > - {showInheritedShareContent && ( - access.document.id !== doc.id, - ) ?? [] - } - /> - )} - {showMemberSection && isRootDoc && ( - - - - - - )} - - {!showMemberSection && canShare && ( - - )} - - )} - - - - - {showFooter && } - - - - - ); -}; - -interface QuickSearchInviteInputSectionProps { - onSelect: (usr: User) => void; - searchUsersRawData: User[] | undefined; - userQuery: string; - minLength: number; -} - -const QuickSearchInviteInputSection = ({ - onSelect, - searchUsersRawData, - userQuery, - minLength, -}: QuickSearchInviteInputSectionProps) => { - const { t } = useTranslation(); - const hint = useMemo(() => { - if (userQuery.length < minLength) { - return t('Type at least {{minLength}} characters to display user names', { - minLength, - }); - } - if (isValidEmail(userQuery)) { - return t('Choose the email'); - } - if (!searchUsersRawData?.length) { - return t('No results. Type a full email address to invite someone.'); - } - - return t('Choose a user'); - }, [minLength, searchUsersRawData?.length, t, userQuery]); - - useEffect(() => { - announce(hint, 'polite'); - }, [hint]); - - const searchUserData: QuickSearchData = useMemo(() => { - const users = searchUsersRawData || []; - const isEmail = isValidEmail(userQuery); - const newUser: User = { - id: userQuery, - full_name: '', - email: userQuery, - short_name: '', - language: '', - is_first_connection: false, - }; - - const hasEmailInUsers = users.some( - (user) => user.email.toLowerCase() === userQuery.toLowerCase(), - ); - - return { - groupName: hint, - elements: users, - endActions: - isEmail && !hasEmailInUsers - ? [ - { - content: , - onSelect: () => void onSelect(newUser), - }, - ] - : undefined, - }; - }, [searchUsersRawData, userQuery, hint, onSelect]); - - return ( - - } - /> - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx deleted file mode 100644 index fee14913c..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; -import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; - -import { Box, HorizontalSeparator, Icon } from '@/components'; -import { Doc, useCopyDocLink } from '@/docs/doc-management'; - -import { DocVisibility } from './DocVisibility'; - -type DocShareModalFooterProps = { - doc: Doc; - onClose: () => void; -}; - -export const DocShareModalFooter = ({ - doc, - onClose, -}: DocShareModalFooterProps) => { - const copyDocLink = useCopyDocLink(doc.id); - const { t } = useTranslation(); - return ( - - - - - - - - - - - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx deleted file mode 100644 index 3a8d2ef83..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import { - VariantType, - useToastProvider, -} from '@gouvfr-lasuite/cunningham-react'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { css } from 'styled-components'; - -import { - Box, - DropdownMenu, - DropdownMenuOption, - Icon, - Text, -} from '@/components'; -import { useCunninghamTheme } from '@/cunningham'; -import { - Doc, - KEY_DOC, - KEY_LIST_DOC, - LinkReach, - LinkRole, - getDocLinkReach, - getDocLinkRole, - useDocUtils, -} from '@/docs/doc-management'; -import { useResponsiveStore } from '@/stores'; - -import { useUpdateDocLink } from '../api/useUpdateDocLink'; -import { useTranslatedShareSettings } from '../hooks/'; - -import { DocDesynchronized } from './DocDesynchronized'; - -interface DocVisibilityProps { - doc: Doc; -} - -export const DocVisibility = ({ doc }: DocVisibilityProps) => { - const { t } = useTranslation(); - const { isDesktop } = useResponsiveStore(); - const { spacingsTokens } = useCunninghamTheme(); - const canManage = doc.abilities.accesses_manage; - const docLinkReach = getDocLinkReach(doc); - const docLinkRole = getDocLinkRole(doc); - const { isDesynchronized } = useDocUtils(doc); - const { linkModeTranslations, linkReachChoices, linkReachTranslations } = - useTranslatedShareSettings(); - const { toast } = useToastProvider(); - - const description = - docLinkRole === LinkRole.READER - ? linkReachChoices[docLinkReach].descriptionReadOnly - : linkReachChoices[docLinkReach].descriptionEdit; - - const { mutate: updateDocLink } = useUpdateDocLink({ - listInvalidQueries: [KEY_LIST_DOC, KEY_DOC], - onSuccess: () => { - toast( - t('The document visibility has been updated.'), - VariantType.SUCCESS, - { - duration: 2000, - }, - ); - }, - }); - - const linkReachOptions: DropdownMenuOption[] = useMemo(() => { - return Object.values(LinkReach).map((key) => { - const isDisabled = doc.abilities.link_select_options[key] === undefined; - let linkRole = undefined; - if (key !== LinkReach.RESTRICTED) { - linkRole = docLinkRole; - } - - return { - label: linkReachTranslations[key], - callback: () => - updateDocLink({ - id: doc.id, - link_reach: key, - link_role: linkRole, - }), - isSelected: docLinkReach === key, - disabled: isDisabled, - }; - }); - }, [ - doc.abilities.link_select_options, - doc.id, - docLinkReach, - docLinkRole, - linkReachTranslations, - updateDocLink, - ]); - - const haveDisabledOptions = linkReachOptions.some( - (option) => option.disabled, - ); - - const showLinkRoleOptions = - docLinkReach !== LinkReach.RESTRICTED && docLinkRole; - - const linkRoleOptions: DropdownMenuOption[] = useMemo(() => { - const options = doc.abilities.link_select_options[docLinkReach] ?? []; - return Object.values(LinkRole).map((key) => { - const isDisabled = !options.includes(key); - return { - label: linkModeTranslations[key], - callback: () => - updateDocLink({ - id: doc.id, - link_role: key, - link_reach: docLinkReach, - }), - isSelected: docLinkRole === key, - disabled: isDisabled, - }; - }); - }, [ - doc.abilities.link_select_options, - doc.id, - docLinkReach, - docLinkRole, - linkModeTranslations, - updateDocLink, - ]); - - const haveDisabledLinkRoleOptions = linkRoleOptions.some( - (option) => option.disabled, - ); - - return ( - - - {t('Link settings')} - - {isDesynchronized && } - - - - - - - {linkReachChoices[docLinkReach].label} - - - - {isDesktop && ( - - {description} - - )} - - {showLinkRoleOptions && ( - - - - {linkModeTranslations[docLinkRole]} - - - - )} - - {!isDesktop && ( - - {description} - - )} - - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx deleted file mode 100644 index f908dee3f..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/SearchUserRow.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { css } from 'styled-components'; - -import { Box, Text } from '@/components'; -import { - QuickSearchItemContent, - QuickSearchItemContentProps, -} from '@/components/quick-search'; -import { useCunninghamTheme } from '@/cunningham'; -import { User, UserAvatar } from '@/features/auth'; - -type Props = { - user: User; - alwaysShowRight?: boolean; - right?: QuickSearchItemContentProps['right']; - isInvitation?: boolean; -}; - -export const SearchUserRow = ({ - user, - right, - alwaysShowRight = false, - isInvitation = false, -}: Props) => { - const hasFullName = !!user.full_name; - const { spacingsTokens, colorsTokens } = useCunninghamTheme(); - - return ( - - - - - {hasFullName ? user.full_name : user.email} - - {hasFullName && ( - - {user.email} - - )} - - - } - /> - ); -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-share/components/index.ts deleted file mode 100644 index da615fb61..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './AlertModalRequestAccess'; -export * from './DocShareModal'; -export * from './DocShareAccessRequest'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/index.ts b/src/frontend/apps/impress/src/features/docs/doc-share/hooks/index.ts deleted file mode 100644 index 6b41a9770..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './useTranslatedShareSettings'; -export * from './useWhoAmI'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useTranslatedShareSettings.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useTranslatedShareSettings.tsx deleted file mode 100644 index d21460998..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useTranslatedShareSettings.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import { LinkReach, LinkRole } from '@/docs/doc-management/types'; - -export const useTranslatedShareSettings = () => { - const { t } = useTranslation(); - - const linkReachTranslations = { - [LinkReach.RESTRICTED]: t('Private'), - [LinkReach.AUTHENTICATED]: t('Connected'), - [LinkReach.PUBLIC]: t('Public'), - }; - - const linkModeTranslations = { - [LinkRole.READER]: t('Reading'), - [LinkRole.EDITOR]: t('Editing'), - }; - - const linkReachChoices = { - [LinkReach.RESTRICTED]: { - label: linkReachTranslations[LinkReach.RESTRICTED], - icon: 'lock', - value: LinkReach.RESTRICTED, - descriptionReadOnly: t('Only invited people can access'), - descriptionEdit: t('Only invited people can access'), - }, - [LinkReach.AUTHENTICATED]: { - label: linkReachTranslations[LinkReach.AUTHENTICATED], - icon: 'vpn_lock', - value: LinkReach.AUTHENTICATED, - descriptionReadOnly: t( - 'Anyone with the link can view the document if they are logged in', - ), - descriptionEdit: t( - 'Anyone with the link can edit the document if they are logged in', - ), - }, - [LinkReach.PUBLIC]: { - label: linkReachTranslations[LinkReach.PUBLIC], - icon: 'public', - value: LinkReach.PUBLIC, - descriptionReadOnly: t('Anyone with the link can see the document'), - descriptionEdit: t('Anyone with the link can edit the document'), - }, - }; - - return { - linkReachTranslations, - linkModeTranslations, - linkReachChoices, - }; -}; 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 deleted file mode 100644 index e418987df..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/hooks/useWhoAmI.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Access, Role } from '@/docs/doc-management'; -import { useAuth } from '@/features/auth'; - -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; - - const isLastOwner = - !rolesAllowed.length && access.role === Role.OWNER && isMyself; - - const isOtherOwner = access.role === Role.OWNER && user?.id && !isMyself; - - return { - isLastOwner, - isOtherOwner, - isMyself, - }; -}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/index.ts b/src/frontend/apps/impress/src/features/docs/doc-share/index.ts deleted file mode 100644 index 14fa9fe25..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './api'; -export * from './components'; -export * from './hooks'; -export * from './types'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx deleted file mode 100644 index aab998069..000000000 --- a/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Role } from '@/docs/doc-management'; -import { User } from '@/features/auth'; - -export interface Invitation { - id: string; - role: Role; - document: string; - created_at: string; - is_expired: boolean; - issuer: string; - email: string; - abilities: { - destroy: boolean; - retrieve: boolean; - partial_update: boolean; - update: boolean; - }; -} - -/** - * Type guard to check if an object is an Invitation - * Invitation has unique properties: email, issuer, is_expired, and document as a string - */ -export const isInvitation = (obj: unknown): obj is Invitation => { - return obj !== null && typeof obj === 'object' && 'issuer' in obj; -}; - -export enum OptionType { - INVITATION = 'invitation', - NEW_MEMBER = 'new_member', -} - -export const isOptionNewMember = ( - data: OptionSelect, -): data is OptionNewMember => { - return 'id' in data.value; -}; - -export interface OptionInvitation { - value: { email: string }; - label: string; - type: OptionType.INVITATION; -} - -export interface OptionNewMember { - value: User; - label: string; - type: OptionType.NEW_MEMBER; -} - -export type OptionSelect = OptionNewMember | OptionInvitation; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx index f43aff36f..add32e3db 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocMoveModal.tsx @@ -21,14 +21,6 @@ import { useResponsiveStore } from '@/stores'; import { DocsGridItemDate, DocsGridItemTitle } from './DocsGridItem'; -const AlertModalRequestAccess = dynamic( - () => - import('@/docs/doc-share/components/AlertModalRequestAccess').then( - (mod) => ({ default: mod.AlertModalRequestAccess }), - ), - { ssr: false }, -); - const ModalConfirmationMoveDoc = dynamic( () => import('./ModalConfimationMoveDoc').then((mod) => ({ @@ -96,7 +88,6 @@ export const DocMoveModal = ({ const { untitledDocument } = useTrans(); const docTargetTitle = docSelected?.title || untitledDocument; const modalConfirmation = useModal(); - const modalRequest = useModal(); const { mutateAsync: moveDoc } = useMoveDoc(); const [search, setSearch] = useState(''); const { isDesktop, isTablet, isMobile } = useResponsiveStore(); @@ -155,7 +146,6 @@ export const DocMoveModal = ({ fullWidth onClick={() => { if (!docSelected?.abilities.move) { - modalRequest.open(); return; } @@ -292,19 +282,6 @@ export const DocMoveModal = ({ targetDocumentTitle={docTargetTitle} /> )} - {modalRequest.isOpen && docSelected?.id && ( - { - modalRequest.onClose(); - onClose(); - }} - targetDocumentTitle={docTargetTitle} - title={t('Move document')} - /> - )} ); }; 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 430df4711..87bba2027 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 @@ -7,10 +7,8 @@ import { useTranslation } from 'react-i18next'; import ContentCopySVG from '@/assets/icons/ui-kit/content_copy.svg'; import DeleteSVG from '@/assets/icons/ui-kit/delete.svg'; 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 LeaveSVG from '@/assets/icons/ui-kit/leave.svg'; import MoreSVG from '@/assets/icons/ui-kit/more_horiz.svg'; import { Doc, @@ -26,14 +24,6 @@ import { useFocusStore } from '@/stores'; import { DocMoveModal } from './DocMoveModal'; -const DocShareModal = dynamic( - () => - import('@/docs/doc-share/components/DocShareModal').then((mod) => ({ - default: mod.DocShareModal, - })), - { ssr: false }, -); - const ModalRemoveDoc = dynamic( () => import('@/docs/doc-management/components/ModalRemoveDoc').then((mod) => ({ @@ -42,16 +32,6 @@ 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; } @@ -61,8 +41,6 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => { 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(); @@ -99,15 +77,6 @@ export const DocsGridActions = ({ doc }: DocsGridActionsProps) => { testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, showSeparator: true, }, - { - label: t('Share'), - icon: