diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index 1a7a6d0994..c85822653c 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -11,14 +11,13 @@ import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; import { connect } from 'react-redux'; import errorService from 'services/error.service'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; -import shareService, { copyTextToClipboard, getSharingRoles } from 'app/share/services/share.service'; +import shareService, { getSharingRoles } from 'app/share/services/share.service'; import { AdvancedSharedItem } from 'app/share/types'; import { isUserItemOwner } from 'views/Shared/utils/sharedViewUtils'; import { sharedThunks } from 'app/store/slices/sharedLinks'; import workspacesSelectors from 'app/store/slices/workspaces/workspaces.selectors'; import ShareInviteDialog from '../ShareInviteDialog/ShareInviteDialog'; import './ShareDialog.scss'; -import envService from 'services/env.service'; import { AccessMode, InvitedUserProps, UserRole, ViewProps, Views } from './types'; import navigationService from 'services/navigation.service'; import { Service } from '@internxt/sdk/dist/drive/payments/types/tiers'; @@ -31,8 +30,9 @@ import { UserRoleSelection } from './components/GeneralView/UserRoleSelection'; import { InvitedUsersList } from './components/GeneralView/InvitedUsersList'; import { Header } from './components/Header'; import { cropSharedName, filterEditorAndReader, getLocalUserData, isAdvancedShareItem } from './utils'; +import { useShareItemActions } from './hooks/useShareItemActions'; -type ShareDialogProps = { +export type ShareDialogProps = { user: UserSettings; isDriveItem?: boolean; onShareItem?: () => void; @@ -81,6 +81,14 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { }); const isProtectWithPasswordOptionAvailable = accessMode === 'public' && !isLoading && isUserOwner; const closeSelectedUserPopover = () => setSelectedUserListIndex(null); + const { getPrivateShareLink } = useShareItemActions({ + dispatch, + isPasswordSharingAvailable, + itemToShare, + onClose: () => dispatch(uiActions.setIsShareDialogOpen(false)), + onShareItem: props.onShareItem, + onStopSharingItem: props.onStopSharingItem, + }); const resetDialogData = () => { setSelectedUserListIndex(null); @@ -192,18 +200,6 @@ const ShareDialog = (props: ShareDialogProps): JSX.Element => { dispatch(uiActions.setIsShareDialogOpen(false)); }; - const getPrivateShareLink = async () => { - try { - await copyTextToClipboard(`${envService.getVariable('hostname')}/shared/?folderuuid=${itemToShare?.item.uuid}`); - notificationsService.show({ text: translate('shared-links.toast.copy-to-clipboard'), type: ToastType.Success }); - } catch { - notificationsService.show({ - text: translate('modals.shareModal.errors.copy-to-clipboard'), - type: ToastType.Error, - }); - } - }; - const onCopyLink = async (): Promise => { if (accessMode === 'restricted') { await getPrivateShareLink(); diff --git a/src/app/drive/components/ShareDialog/ShareDialogWrapper.tsx b/src/app/drive/components/ShareDialog/ShareDialogWrapper.tsx new file mode 100644 index 0000000000..ad6cb69ceb --- /dev/null +++ b/src/app/drive/components/ShareDialog/ShareDialogWrapper.tsx @@ -0,0 +1,8 @@ +import { ShareDialogProvider } from './context/ShareDialogContextProvider'; +import ShareDialog, { ShareDialogProps } from './ShareDialog'; + +export const ShareDialogWrapper = (props: Omit) => ( + + + +); diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemActions.test.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemActions.test.ts new file mode 100644 index 0000000000..05e3a9779e --- /dev/null +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemActions.test.ts @@ -0,0 +1,469 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, vi, beforeEach, test } from 'vitest'; +import { useShareItemActions } from './useShareItemActions'; +import shareService from 'app/share/services/share.service'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import errorService from 'services/error.service'; +import { ItemToShare } from 'app/store/slices/storage/types'; +import envService from 'services/env.service'; +import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; + +const { mockActionDispatch, mockUseShareDialogContext, mockTranslate, mockDispatch } = vi.hoisted(() => ({ + mockActionDispatch: vi.fn(), + mockUseShareDialogContext: vi.fn(), + mockTranslate: vi.fn((key: string) => key), + mockDispatch: vi.fn(), +})); + +vi.mock('utils/copyToClipboard.utils'); +vi.mock('app/i18n/provider/TranslationProvider', () => ({ + useTranslationContext: () => ({ translate: mockTranslate }), +})); +vi.mock('../context/ShareDialogContextProvider', () => ({ + useShareDialogContext: mockUseShareDialogContext, + ShareDialogProvider: ({ children }: { children: React.ReactNode }) => children, +})); +vi.mock('app/store/slices/sharedLinks', () => ({ + default: () => ({}), + sharedThunks: { + stopSharingItem: vi.fn(), + removeUserFromSharedFolder: vi.fn(), + }, + sharedActions: {}, +})); + +const createItemToShare = (isFolder: boolean, uuid = 'item-uuid-123'): ItemToShare => ({ + item: { + id: 1, + uuid, + name: 'Test Item', + isFolder, + } as any, +}); + +describe('Share Item Actions', () => { + const mockOnClose = vi.fn(); + const mockOnShareItem = vi.fn(); + const mockOnStopSharingItem = vi.fn(); + + const mockSharingMeta = { + id: 'sharing-id-123', + encryptedCode: 'encrypted-code', + token: 'share-token', + code: 'share-code', + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'restricted', + sharingMeta: null, + isPasswordProtected: false, + }, + dispatch: mockActionDispatch, + }); + vi.spyOn(envService, 'getVariable').mockReturnValue('https://example.com'); + }); + + describe('Get Private Share Link', () => { + test('When copying private share link successfully, then shows success notification', async () => { + const mockedCopyToClipboard = vi.mocked(copyTextToClipboard); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.getPrivateShareLink(); + + expect(mockedCopyToClipboard).toHaveBeenCalledWith('https://example.com/shared/?folderuuid=item-uuid-123'); + expect(showNotificationSpy).toHaveBeenCalledWith({ + text: 'shared-links.toast.copy-to-clipboard', + type: ToastType.Success, + }); + }); + + test('When copying private share link fails, then shows error notification', async () => { + vi.mocked(copyTextToClipboard).mockRejectedValue(new Error()); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.getPrivateShareLink(); + + expect(showNotificationSpy).toHaveBeenCalledWith({ + text: 'modals.shareModal.errors.copy-to-clipboard', + type: ToastType.Error, + }); + }); + }); + + describe('Copy Link', () => { + test('When access mode is restricted, then copies private share link', async () => { + const mockedCopyToClipboard = vi.mocked(copyTextToClipboard); + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.onCopyLink(); + + expect(mockedCopyToClipboard).toHaveBeenCalled(); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SELECTED_USER_LIST_INDEX', payload: null }), + ); + }); + + test('When access mode is public, then gets public share link', async () => { + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'public', + sharingMeta: null, + isPasswordProtected: false, + }, + dispatch: mockActionDispatch, + }); + const getPublicShareLinkSpy = vi + .spyOn(shareService, 'getPublicShareLink') + .mockResolvedValue(mockSharingMeta as any); + + const itemToShare = createItemToShare(false); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + onShareItem: mockOnShareItem, + }), + ); + + await result.current.onCopyLink(); + + expect(getPublicShareLinkSpy).toHaveBeenCalledWith('item-uuid-123', 'file', undefined); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SHARING_META', payload: mockSharingMeta }), + ); + expect(mockOnShareItem).toHaveBeenCalled(); + }); + }); + + describe('Password Checkbox Change', () => { + test('When password sharing is not available, then opens restricted password dialog', () => { + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: false, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + result.current.onPasswordCheckboxChange(); + + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_IS_RESTRICTED_PASSWORD_DIALOG_OPEN', payload: true }), + ); + }); + + test('When password is already protected, then opens password disable dialog', () => { + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'public', + sharingMeta: mockSharingMeta, + isPasswordProtected: true, + }, + dispatch: mockActionDispatch, + }); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + result.current.onPasswordCheckboxChange(); + + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_OPEN_PASSWORD_DISABLE_DIALOG', payload: true }), + ); + }); + + test('When password is not protected, then opens password input', () => { + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + result.current.onPasswordCheckboxChange(); + + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_OPEN_PASSWORD_INPUT', payload: true }), + ); + }); + }); + + describe('Save Public Share Password', () => { + test('When sharing info exists, then saves password', async () => { + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'public', + sharingMeta: mockSharingMeta, + isPasswordProtected: false, + }, + dispatch: mockActionDispatch, + }); + const saveSharingPasswordSpy = vi + .spyOn(shareService, 'saveSharingPassword') + .mockResolvedValue(mockSharingMeta as any); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + onShareItem: mockOnShareItem, + }), + ); + + await result.current.onSavePublicSharePassword('my-password'); + + expect(saveSharingPasswordSpy).toHaveBeenCalledWith('sharing-id-123', 'my-password', 'encrypted-code'); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_IS_PASSWORD_PROTECTED', payload: true }), + ); + expect(mockOnShareItem).toHaveBeenCalled(); + }); + + test('When sharing info does not exist, then creates new public share with password', async () => { + const createPublicShareFromOwnerUserSpy = vi + .spyOn(shareService, 'createPublicShareFromOwnerUser') + .mockResolvedValue(mockSharingMeta as any); + + const itemToShare = createItemToShare(false); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + onShareItem: mockOnShareItem, + }), + ); + + await result.current.onSavePublicSharePassword('my-password'); + + expect(createPublicShareFromOwnerUserSpy).toHaveBeenCalledWith('item-uuid-123', 'file', 'my-password'); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SHARING_META', payload: mockSharingMeta }), + ); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_IS_PASSWORD_PROTECTED', payload: true }), + ); + }); + + test('When error occurs, then casts error and closes password input', async () => { + const error = new Error('Save password failed'); + vi.spyOn(shareService, 'createPublicShareFromOwnerUser').mockRejectedValue(error); + const castErrorSpy = vi.spyOn(errorService, 'castError'); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.onSavePublicSharePassword('my-password'); + + expect(castErrorSpy).toHaveBeenCalledWith(error); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_OPEN_PASSWORD_INPUT', payload: false }), + ); + }); + }); + + describe('Disable Password', () => { + test('When disabling password successfully, then removes password protection', async () => { + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'public', + sharingMeta: mockSharingMeta, + isPasswordProtected: true, + }, + dispatch: mockActionDispatch, + }); + const removeSharingPasswordSpy = vi.spyOn(shareService, 'removeSharingPassword').mockResolvedValue(undefined); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.onDisablePassword(); + + expect(removeSharingPasswordSpy).toHaveBeenCalledWith('sharing-id-123'); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_IS_PASSWORD_PROTECTED', payload: false }), + ); + }); + + test('When error occurs, then casts error and closes dialog', async () => { + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'public', + sharingMeta: mockSharingMeta, + isPasswordProtected: true, + }, + dispatch: mockActionDispatch, + }); + const error = new Error('Remove password failed'); + vi.spyOn(shareService, 'removeSharingPassword').mockRejectedValue(error); + const castErrorSpy = vi.spyOn(errorService, 'castError'); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.onDisablePassword(); + + expect(castErrorSpy).toHaveBeenCalledWith(error); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_OPEN_PASSWORD_DISABLE_DIALOG', payload: false }), + ); + }); + }); + + describe('Stop Sharing', () => { + test('When stopping sharing, then triggers the action and closes dialog', async () => { + mockDispatch.mockResolvedValue({ payload: true }); + + const itemToShare = createItemToShare(true, 'folder-uuid-456'); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + onShareItem: mockOnShareItem, + onStopSharingItem: mockOnStopSharingItem, + }), + ); + + await result.current.onStopSharing(); + + expect(mockDispatch).toHaveBeenCalled(); + expect(mockOnShareItem).toHaveBeenCalled(); + expect(mockOnStopSharingItem).toHaveBeenCalled(); + expect(mockOnClose).toHaveBeenCalled(); + }); + }); + + describe('Remove User', () => { + test('When removing user successfully, then removes user from state', async () => { + mockDispatch.mockResolvedValue({ payload: true }); + + const user = { + avatar: null, + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + roleName: 'editor' as const, + uuid: 'user-uuid-123', + sharingId: 'sharing-id-456', + }; + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.onRemoveUser(user); + + expect(mockDispatch).toHaveBeenCalled(); + expect(mockActionDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'REMOVE_USER', payload: 'user-uuid-123' }), + ); + expect(mockOnClose).toHaveBeenCalled(); + }); + + test('When user removal fails, then still closes dialog', async () => { + mockDispatch.mockResolvedValue({ payload: false }); + + const user = { + avatar: null, + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + roleName: 'editor' as const, + uuid: 'user-uuid-123', + sharingId: 'sharing-id-456', + }; + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => + useShareItemActions({ + itemToShare, + isPasswordSharingAvailable: true, + dispatch: mockDispatch, + onClose: mockOnClose, + }), + ); + + await result.current.onRemoveUser(user); + + expect(mockOnClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemActions.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemActions.ts new file mode 100644 index 0000000000..a051e2ae15 --- /dev/null +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemActions.ts @@ -0,0 +1,177 @@ +import { useCallback } from 'react'; +import { useShareDialogContext } from '../context/ShareDialogContextProvider'; +import { + removeUser, + setIsLoading, + setIsPasswordProtected, + setIsRestrictedPasswordDialogOpen, + setOpenPasswordDisableDialog, + setOpenPasswordInput, + setSelectedUserListIndex, + setSharingMeta, + setShowStopSharingConfirmation, +} from '../context/ShareDialogContext.actions'; +import { cropSharedName, isAdvancedShareItem } from '../utils'; +import { sharedThunks } from 'app/store/slices/sharedLinks'; +import { ItemToShare } from 'app/store/slices/storage/types'; +import shareService from 'app/share/services/share.service'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { InvitedUserProps } from '../types'; +import errorService from 'services/error.service'; +import envService from 'services/env.service'; +import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; +import { AppDispatch } from 'app/store'; + +interface ShareItemActionsProps { + itemToShare: ItemToShare | null; + isPasswordSharingAvailable: boolean; + dispatch: AppDispatch; + onClose: () => void; + onShareItem?: () => void; + onStopSharingItem?: () => void; +} + +export const useShareItemActions = ({ + itemToShare, + isPasswordSharingAvailable, + dispatch, + ...props +}: ShareItemActionsProps) => { + const { translate } = useTranslationContext(); + const { state, dispatch: actionDispatch } = useShareDialogContext(); + + const { accessMode, sharingMeta, isPasswordProtected } = state; + + const getPrivateShareLink = async () => { + try { + await copyTextToClipboard(`${envService.getVariable('hostname')}/shared/?folderuuid=${itemToShare?.item.uuid}`); + notificationsService.show({ text: translate('shared-links.toast.copy-to-clipboard'), type: ToastType.Success }); + } catch { + notificationsService.show({ + text: translate('modals.shareModal.errors.copy-to-clipboard'), + type: ToastType.Error, + }); + } + }; + + const onCopyLink = async (): Promise => { + if (accessMode === 'restricted') { + await getPrivateShareLink(); + actionDispatch(setSelectedUserListIndex(null)); + return; + } + + if (itemToShare?.item.uuid) { + const encryptionKey = isAdvancedShareItem(itemToShare.item) ? itemToShare?.item?.encryptionKey : undefined; + const sharingInfo = await shareService.getPublicShareLink( + itemToShare?.item.uuid, + itemToShare.item.isFolder ? 'folder' : 'file', + encryptionKey, + ); + + if (sharingInfo) { + actionDispatch(setSharingMeta(sharingInfo)); + } + + props.onShareItem?.(); + actionDispatch(setSelectedUserListIndex(null)); + } + }; + + const onPasswordCheckboxChange = useCallback(() => { + if (!isPasswordSharingAvailable) { + actionDispatch(setIsRestrictedPasswordDialogOpen(true)); + return; + } + + if (isPasswordProtected) { + actionDispatch(setOpenPasswordDisableDialog(true)); + } else { + actionDispatch(setOpenPasswordInput(true)); + } + }, [isPasswordProtected, isPasswordSharingAvailable]); + + const onSavePublicSharePassword = useCallback( + async (plainPassword: string) => { + try { + let sharingInfo = sharingMeta; + + if (sharingInfo?.encryptedCode) { + await shareService.saveSharingPassword(sharingInfo.id, plainPassword, sharingInfo.encryptedCode); + } else { + const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; + const itemId = itemToShare?.item.uuid ?? ''; + sharingInfo = await shareService.createPublicShareFromOwnerUser(itemId, itemType, plainPassword); + actionDispatch(setSharingMeta(sharingInfo)); + } + + actionDispatch(setIsPasswordProtected(true)); + props.onShareItem?.(); + } catch (error) { + errorService.castError(error); + } finally { + actionDispatch(setOpenPasswordInput(false)); + } + }, + [sharingMeta, itemToShare], + ); + + const onDisablePassword = useCallback(async () => { + try { + if (sharingMeta) { + await shareService.removeSharingPassword(sharingMeta.id); + actionDispatch(setIsPasswordProtected(false)); + } + } catch (error) { + errorService.castError(error); + } finally { + actionDispatch(setOpenPasswordDisableDialog(false)); + } + }, [sharingMeta]); + + const onStopSharing = async () => { + actionDispatch(setIsLoading(true)); + const itemName = cropSharedName(itemToShare?.item.name as string); + await dispatch( + sharedThunks.stopSharingItem({ + itemType: itemToShare?.item.isFolder ? 'folder' : 'file', + itemId: itemToShare?.item.uuid as string, + itemName, + }), + ); + props.onShareItem?.(); + props.onStopSharingItem?.(); + actionDispatch(setShowStopSharingConfirmation(false)); + props.onClose(); + setIsLoading(false); + }; + + const onRemoveUser = async (user: InvitedUserProps) => { + if (user) { + const hasBeenRemoved = await dispatch( + sharedThunks.removeUserFromSharedFolder({ + itemType: itemToShare?.item.isFolder ? 'folder' : 'file', + itemId: itemToShare?.item.uuid as string, + userId: user.uuid, + userEmail: user.email, + }), + ); + + if (hasBeenRemoved.payload) { + actionDispatch(removeUser(user.uuid)); + } + } + props.onClose(); + }; + + return { + onPasswordCheckboxChange, + onSavePublicSharePassword, + onDisablePassword, + onCopyLink, + onStopSharing, + getPrivateShareLink, + onRemoveUser, + }; +}; diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemInvitations.test.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemInvitations.test.ts new file mode 100644 index 0000000000..d037858ee3 --- /dev/null +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemInvitations.test.ts @@ -0,0 +1,187 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach, test, afterEach } from 'vitest'; +import { useShareItemInvitations } from './useShareItemInvitations'; +import shareService from 'app/share/services/share.service'; +import * as utils from '../utils'; +import { ItemToShare } from 'app/store/slices/storage/types'; + +const { mockDispatch, mockUseShareDialogContext } = vi.hoisted(() => ({ + mockDispatch: vi.fn(), + mockUseShareDialogContext: vi.fn(), +})); + +vi.mock('../utils'); +vi.mock('../context/ShareDialogContextProvider', () => ({ + useShareDialogContext: mockUseShareDialogContext, + ShareDialogProvider: ({ children }: { children: React.ReactNode }) => children, +})); + +describe('Share item Invitations', () => { + const mockRoles = [ + { id: '1', name: 'OWNER', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + { id: '2', name: 'EDITOR', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + { id: '3', name: 'READER', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + ]; + + const mockOwnerData = { + avatar: null, + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + uuid: 'user-uuid-123', + sharingId: '', + role: { + id: 'NONE', + name: 'OWNER', + createdAt: '', + updatedAt: '', + }, + }; + + const mockUsers = [ + { + email: 'user1@example.com', + role: { id: '2', name: 'EDITOR' }, + name: 'User', + lastname: 'One', + }, + { + email: 'user2@example.com', + role: { id: '3', name: 'READER' }, + name: 'User', + lastname: 'Two', + }, + ]; + + const createItemToShare = (isFolder: boolean, uuid = 'item-uuid-123'): ItemToShare => ({ + item: { + id: 1, + uuid, + name: 'Test Item', + isFolder, + } as any, + }); + + beforeEach(() => { + vi.clearAllMocks(); + mockUseShareDialogContext.mockReturnValue({ + state: { roles: mockRoles }, + dispatch: mockDispatch, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Inviting a user', () => { + test('When the user wants to share an item, then the invite modal is opened', () => { + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemInvitations({ itemToShare, isUserOwner: true })); + + result.current.onInviteUser(); + + expect(mockDispatch).toHaveBeenCalledTimes(2); + expect(mockDispatch).toHaveBeenCalledWith(expect.objectContaining({ type: 'SET_VIEW', payload: 'invite' })); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SELECTED_USER_LIST_INDEX', payload: null }), + ); + }); + }); + + describe('Get and update invited users', () => { + test('When there is no item, then does not fetch users', async () => { + const getUsersOfSharedFolderSpy = vi.spyOn(shareService, 'getUsersOfSharedFolder'); + const { result } = renderHook(() => + useShareItemInvitations({ itemToShare: { item: undefined } as any, isUserOwner: true }), + ); + + await result.current.getAndUpdateInvitedUsers(); + + expect(getUsersOfSharedFolderSpy).not.toHaveBeenCalled(); + }); + + test('When the item is a folder, then fetches users with folder type', async () => { + const getUSerOfSharedFolderSpy = vi + .spyOn(shareService, 'getUsersOfSharedFolder') + .mockResolvedValue({ users: mockUsers } as any); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemInvitations({ itemToShare, isUserOwner: true })); + + await result.current.getAndUpdateInvitedUsers(); + + expect(getUSerOfSharedFolderSpy).toHaveBeenCalledWith({ + itemType: 'folder', + folderId: 'item-uuid-123', + }); + }); + + test('When the item is a file, then fetches users with file type', async () => { + const getUSerOfSharedFolderSpy = vi + .spyOn(shareService, 'getUsersOfSharedFolder') + .mockResolvedValue({ users: mockUsers } as any); + + const itemToShare = createItemToShare(false); + const { result } = renderHook(() => useShareItemInvitations({ itemToShare, isUserOwner: true })); + + await result.current.getAndUpdateInvitedUsers(); + + expect(getUSerOfSharedFolderSpy).toHaveBeenCalledWith({ + itemType: 'file', + folderId: 'item-uuid-123', + }); + }); + + it('When users are fetched successfully, then updates invited users with roleName', async () => { + vi.spyOn(shareService, 'getUsersOfSharedFolder').mockResolvedValue({ users: mockUsers } as any); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemInvitations({ itemToShare, isUserOwner: true })); + + await result.current.getAndUpdateInvitedUsers(); + + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SET_INVITED_USERS', + payload: [ + { ...mockUsers[0], roleName: 'editor' }, + { ...mockUsers[1], roleName: 'reader' }, + ], + }), + ); + }); + + test('When an error occurs and user is owner, then sets owner data as invited user', async () => { + const unexpectedError = new Error('No users found'); + vi.spyOn(shareService, 'getUsersOfSharedFolder').mockRejectedValue(unexpectedError); + vi.mocked(utils.getLocalUserData).mockReturnValue(mockOwnerData); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemInvitations({ itemToShare, isUserOwner: true })); + + await result.current.getAndUpdateInvitedUsers(); + + expect(utils.getLocalUserData).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SET_INVITED_USERS', + payload: [{ ...mockOwnerData, roleName: 'owner' }], + }), + ); + }); + + test('When an error occurs and user is not owner, then nothing happens', async () => { + const unexpectedError = new Error('No users found'); + vi.spyOn(shareService, 'getUsersOfSharedFolder').mockRejectedValue(unexpectedError); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemInvitations({ itemToShare, isUserOwner: false })); + + await result.current.getAndUpdateInvitedUsers(); + + expect(utils.getLocalUserData).not.toHaveBeenCalled(); + expect(mockDispatch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemInvitations.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemInvitations.ts new file mode 100644 index 0000000000..719b166073 --- /dev/null +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemInvitations.ts @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import shareService from 'app/share/services/share.service'; +import { setInvitedUsers, setSelectedUserListIndex, setView } from '../context/ShareDialogContext.actions'; +import { getLocalUserData } from '../utils'; +import { useShareDialogContext } from '../context/ShareDialogContextProvider'; +import { ItemToShare } from 'app/store/slices/storage/types'; + +interface ShareItemInvitationsProps { + itemToShare: ItemToShare | null; + isUserOwner: boolean; +} + +export const useShareItemInvitations = ({ itemToShare, isUserOwner }: ShareItemInvitationsProps) => { + const { state, dispatch: actionDispatch } = useShareDialogContext(); + + const { roles } = state; + + const getAndUpdateInvitedUsers = useCallback(async () => { + if (!itemToShare?.item) return; + + try { + const invitedUsersList = await shareService.getUsersOfSharedFolder({ + itemType: itemToShare.item.isFolder ? 'folder' : 'file', + folderId: itemToShare.item.uuid, + }); + + const invitedUsersListParsed = invitedUsersList['users'].map((user) => ({ + ...user, + roleName: roles.find((role) => role.id === user.role.id)?.name.toLowerCase(), + })); + + actionDispatch(setInvitedUsers(invitedUsersListParsed)); + } catch { + // the server throws an error when there are no users with shared item, + // that means that the local user is the owner as there is nobody else with this shared file. + if (isUserOwner) { + const ownerData = getLocalUserData(); + actionDispatch(setInvitedUsers([{ ...ownerData, roleName: 'owner' }])); + } + } + }, [itemToShare, roles, isUserOwner, actionDispatch]); + + const onInviteUser = () => { + actionDispatch(setView('invite')); + actionDispatch(setSelectedUserListIndex(null)); + }; + + return { + onInviteUser, + getAndUpdateInvitedUsers, + }; +}; diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.test.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.test.ts new file mode 100644 index 0000000000..abe2d8f221 --- /dev/null +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.test.ts @@ -0,0 +1,258 @@ +import { renderHook } from '@testing-library/react'; +import { describe, expect, vi, beforeEach, test, afterEach } from 'vitest'; +import { useShareItemUserRoles } from './useShareItemUserRoles'; +import shareService from 'app/share/services/share.service'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import errorService from 'services/error.service'; +import { ItemToShare } from 'app/store/slices/storage/types'; + +const { mockDispatch, mockUseShareDialogContext, mockTranslate } = vi.hoisted(() => ({ + mockDispatch: vi.fn(), + mockUseShareDialogContext: vi.fn(), + mockTranslate: vi.fn((key: string) => key), +})); + +vi.mock('app/i18n/provider/TranslationProvider', () => ({ + useTranslationContext: () => ({ translate: mockTranslate }), +})); +vi.mock('../context/ShareDialogContextProvider', () => ({ + useShareDialogContext: mockUseShareDialogContext, + ShareDialogProvider: ({ children }: { children: React.ReactNode }) => children, +})); + +const mockSharingMeta = { + id: 'sharing-id-123', + encryptedCode: 'encrypted-code', + token: 'share-token', + code: 'share-code', +}; + +const mockRoles = [ + { id: '1', name: 'OWNER', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + { id: '2', name: 'EDITOR', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + { id: '3', name: 'READER', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, +]; + +const mockInvitedUsers = [ + { + avatar: null, + name: 'User', + lastname: 'One', + email: 'user1@example.com', + roleName: 'editor' as const, + uuid: 'user-uuid-1', + sharingId: 'sharing-id-1', + }, + { + avatar: null, + name: 'User', + lastname: 'Two', + email: 'user2@example.com', + roleName: 'reader' as const, + uuid: 'user-uuid-2', + sharingId: 'sharing-id-2', + }, +]; + +const createItemToShare = (isFolder: boolean, uuid = 'item-uuid-123'): ItemToShare => ({ + item: { + id: 1, + uuid, + name: 'Test Item', + isFolder, + } as any, +}); + +describe('Share Items User Roles', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'restricted', + roles: mockRoles, + invitedUsers: mockInvitedUsers, + }, + dispatch: mockDispatch, + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('Change Item Access', () => { + test('When restricted sharing is not available, then opens restricted sharing dialog', async () => { + const updateSharingTypeSpy = vi.spyOn(shareService, 'updateSharingType'); + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: false, itemToShare })); + + await result.current.changeAccess('public'); + + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SELECTED_USER_LIST_INDEX', payload: null }), + ); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_IS_RESTRICTED_SHARING_DIALOG_OPEN', payload: true }), + ); + expect(updateSharingTypeSpy).not.toHaveBeenCalled(); + }); + + test('When access mode is the same, then nothing happens', async () => { + const updateSharingTypeSpy = vi.spyOn(shareService, 'updateSharingType'); + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.changeAccess('restricted'); + + expect(updateSharingTypeSpy).not.toHaveBeenCalled(); + }); + + test('When changing to restricted mode for folder, then updates sharing type to private', async () => { + mockUseShareDialogContext.mockReturnValue({ + state: { + accessMode: 'public', + roles: mockRoles, + invitedUsers: mockInvitedUsers, + }, + dispatch: mockDispatch, + }); + const updateShareTypeSpy = vi.spyOn(shareService, 'updateSharingType').mockResolvedValue(undefined); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.changeAccess('restricted'); + + expect(updateShareTypeSpy).toHaveBeenCalledWith('item-uuid-123', 'folder', 'private'); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_ACCESS_MODE', payload: 'restricted' }), + ); + }); + + test('When changing to public mode for file, then updates sharing type and creates public share', async () => { + const mockShareInfo = { token: 'share-token', code: 'share-code' }; + const updateShareTypeSpy = vi.spyOn(shareService, 'updateSharingType').mockResolvedValue(undefined); + const createPublicShareFromOwnerUserSpy = vi + .spyOn(shareService, 'createPublicShareFromOwnerUser') + .mockResolvedValue(mockShareInfo as any); + + const itemToShare = createItemToShare(false); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.changeAccess('public'); + + expect(updateShareTypeSpy).toHaveBeenCalledWith('item-uuid-123', 'file', 'public'); + expect(createPublicShareFromOwnerUserSpy).toHaveBeenCalledWith('item-uuid-123', 'file'); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SHARING_META', payload: mockShareInfo }), + ); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_IS_PASSWORD_PROTECTED', payload: false }), + ); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_ACCESS_MODE', payload: 'public' }), + ); + }); + + test('When changing access mode sets loading states correctly', async () => { + vi.spyOn(shareService, 'updateSharingType').mockResolvedValue(undefined); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.changeAccess('public'); + + const calls = mockDispatch.mock.calls; + expect(calls[1]).toEqual([expect.objectContaining({ type: 'SET_IS_LOADING', payload: true })]); + expect(calls.at(-1)).toEqual([expect.objectContaining({ type: 'SET_IS_LOADING', payload: false })]); + }); + + test('When error occurs, then shows error notification and reports error', async () => { + const error = new Error('Update failed'); + vi.spyOn(shareService, 'updateSharingType').mockRejectedValue(error); + const reportErrorSpy = vi.spyOn(errorService, 'reportError').mockReturnValue(undefined); + const showNotificationSpy = vi.spyOn(notificationsService, 'show'); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.changeAccess('public'); + + expect(reportErrorSpy).toHaveBeenCalledWith(error); + expect(showNotificationSpy).toHaveBeenCalledWith({ + text: 'modals.shareModal.errors.update-sharing-access', + type: ToastType.Error, + }); + }); + }); + + describe('User Role Change Handler', () => { + test('When changing user role successfully, then updates role in state', async () => { + const updateUserRoleOfSharedFolderSpy = vi + .spyOn(shareService, 'updateUserRoleOfSharedFolder') + .mockResolvedValue(mockSharingMeta as any); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.handleUserRoleChange('user1@example.com', 'reader'); + + expect(updateUserRoleOfSharedFolderSpy).toHaveBeenCalledWith({ + sharingId: 'sharing-id-1', + newRoleId: '3', + }); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'SET_INVITED_USERS', + payload: [{ ...mockInvitedUsers[0], roleId: '3', roleName: 'reader' }, mockInvitedUsers[1]], + }), + ); + expect(mockDispatch).toHaveBeenCalledWith( + expect.objectContaining({ type: 'SET_SELECTED_USER_LIST_INDEX', payload: null }), + ); + }); + + test('When user email is not found, then nothing happens', async () => { + const itemToShare = createItemToShare(true); + const updateUserRoleOfSharedFolderSpy = vi + .spyOn(shareService, 'updateUserRoleOfSharedFolder') + .mockResolvedValue(mockSharingMeta as any); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.handleUserRoleChange('unknown@example.com', 'reader'); + + expect(updateUserRoleOfSharedFolderSpy).not.toHaveBeenCalled(); + }); + + test('When role name is not found, then does not update role', async () => { + const itemToShare = createItemToShare(true); + const updateUserRoleOfSharedFolderSpy = vi + .spyOn(shareService, 'updateUserRoleOfSharedFolder') + .mockResolvedValue(mockSharingMeta as any); + + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.handleUserRoleChange('user1@example.com', 'ADMIN'); + + expect(updateUserRoleOfSharedFolderSpy).not.toHaveBeenCalled(); + }); + + test('When an error occurs, then shows error notification and reports the error', async () => { + const error = new Error('Update role failed'); + const showNotificationServiceSpy = vi.spyOn(notificationsService, 'show'); + vi.spyOn(shareService, 'updateUserRoleOfSharedFolder').mockRejectedValue(error); + const reportErrorSpy = vi.spyOn(errorService, 'reportError').mockReturnValue(undefined); + + const itemToShare = createItemToShare(true); + const { result } = renderHook(() => useShareItemUserRoles({ isRestrictedSharingAvailable: true, itemToShare })); + + await result.current.handleUserRoleChange('user1@example.com', 'reader'); + + expect(reportErrorSpy).toHaveBeenCalledWith(error); + expect(showNotificationServiceSpy).toHaveBeenCalledWith({ + text: 'modals.shareModal.errors.updatingRole', + type: ToastType.Error, + }); + }); + }); +}); diff --git a/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts b/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts new file mode 100644 index 0000000000..8580840621 --- /dev/null +++ b/src/app/drive/components/ShareDialog/hooks/useShareItemUserRoles.ts @@ -0,0 +1,94 @@ +import shareService from 'app/share/services/share.service'; +import { + setAccessMode, + setInvitedUsers, + setIsLoading, + setIsPasswordProtected, + setIsRestrictedSharingDialogOpen, + setSelectedUserListIndex, + setSharingMeta, +} from '../context/ShareDialogContext.actions'; +import { AccessMode, UserRole } from '../types'; +import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; +import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; +import { useShareDialogContext } from '../context/ShareDialogContextProvider'; +import { ItemToShare } from 'app/store/slices/storage/types'; +import errorService from 'services/error.service'; + +interface ShareItemUserRolesProps { + isRestrictedSharingAvailable: boolean; + itemToShare: ItemToShare | null; +} + +export const useShareItemUserRoles = ({ isRestrictedSharingAvailable, itemToShare }: ShareItemUserRolesProps) => { + const { translate } = useTranslationContext(); + const { state, dispatch: actionDispatch } = useShareDialogContext(); + + const { accessMode, roles, invitedUsers } = state; + + const changeAccess = async (mode: AccessMode) => { + actionDispatch(setSelectedUserListIndex(null)); + + if (!isRestrictedSharingAvailable) { + actionDispatch(setIsRestrictedSharingDialogOpen(true)); + return; + } + + if (mode != accessMode) { + actionDispatch(setIsLoading(true)); + try { + const sharingType = mode === 'restricted' ? 'private' : 'public'; + const itemType = itemToShare?.item.isFolder ? 'folder' : 'file'; + const itemId = itemToShare?.item.uuid ?? ''; + + await shareService.updateSharingType(itemId, itemType, sharingType); + + if (sharingType === 'public') { + const shareInfo = await shareService.createPublicShareFromOwnerUser(itemId, itemType); + actionDispatch(setSharingMeta(shareInfo)); + actionDispatch(setIsPasswordProtected(false)); + } + + actionDispatch(setAccessMode(mode)); + } catch (error) { + errorService.reportError(error); + notificationsService.show({ + text: translate('modals.shareModal.errors.update-sharing-access'), + type: ToastType.Error, + }); + } + actionDispatch(setIsLoading(false)); + } + }; + + const handleUserRoleChange = async (email: string, roleName: string) => { + try { + actionDispatch(setSelectedUserListIndex(null)); + const roleId = roles.find((role) => role.name.toLowerCase() === roleName.toLowerCase())?.id; + const sharingId = invitedUsers.find((invitedUser) => invitedUser.email === email)?.sharingId; + + if (roleId && sharingId) { + await shareService.updateUserRoleOfSharedFolder({ + sharingId: sharingId, + newRoleId: roleId, + }); + + const modifiedInvitedUsers = invitedUsers.map((invitedUser) => { + if (invitedUser.email === email) { + return { ...invitedUser, roleId, roleName: roleName as UserRole }; + } + return invitedUser; + }); + actionDispatch(setInvitedUsers(modifiedInvitedUsers)); + } + } catch (error) { + errorService.reportError(error); + notificationsService.show({ text: translate('modals.shareModal.errors.updatingRole'), type: ToastType.Error }); + } + }; + + return { + changeAccess, + handleUserRoleChange, + }; +}; diff --git a/src/app/share/services/share.service.ts b/src/app/share/services/share.service.ts index 7866e3b1ea..6b76c6fb7a 100644 --- a/src/app/share/services/share.service.ts +++ b/src/app/share/services/share.service.ts @@ -22,7 +22,6 @@ import { } from '@internxt/sdk/dist/drive/share/types'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { WorkspaceCredentialsDetails, WorkspaceData } from '@internxt/sdk/dist/workspaces'; -import copy from 'copy-to-clipboard'; import { t } from 'i18next'; import { Iterator } from '../../core/collections'; import { SdkFactory } from '../../core/factory/sdk'; @@ -37,6 +36,7 @@ import notificationsService, { ToastType } from '../../notifications/services/no import { AdvancedSharedItem } from '../types'; import { domainManager } from './DomainManager'; import { generateCaptchaToken } from 'utils'; +import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; interface CreateShareResponse { created: boolean; @@ -354,14 +354,6 @@ export const getPublicShareLink = async ( } }; -export const copyTextToClipboard = async (text: string) => { - try { - await navigator.clipboard.writeText(text); - } catch { - copy(text); - } -}; - interface SharedDirectoryFoldersPayload { token: string; directoryId: number; diff --git a/src/components/Copyable.tsx b/src/components/Copyable.tsx index d0204db436..ce92767c99 100644 --- a/src/components/Copyable.tsx +++ b/src/components/Copyable.tsx @@ -1,7 +1,7 @@ import { Copy } from '@phosphor-icons/react'; import { useState } from 'react'; import Tooltip from './Tooltip'; -import { copyTextToClipboard } from 'app/share/services/share.service'; +import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; interface CopyableProps { className?: string; diff --git a/src/utils/copyToClipboard.utils.ts b/src/utils/copyToClipboard.utils.ts new file mode 100644 index 0000000000..8fd5e30279 --- /dev/null +++ b/src/utils/copyToClipboard.utils.ts @@ -0,0 +1,9 @@ +import copy from 'copy-to-clipboard'; + +export const copyTextToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch { + copy(text); + } +}; diff --git a/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx b/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx index 3a7c4b07cf..11a33dd929 100644 --- a/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx +++ b/src/views/Drive/components/DriveExplorer/DriveExplorer.tsx @@ -56,12 +56,12 @@ import EditItemNameDialog from 'app/drive/components/EditItemNameDialog/EditItem import ItemDetailsDialog from 'app/drive/components/ItemDetailsDialog/ItemDetailsDialog'; import MoveItemsDialog from 'app/drive/components/MoveItemsDialog/MoveItemsDialog'; import NameCollisionContainer from 'app/drive/components/NameCollisionDialog/NameCollisionContainer'; -import ShareDialog from 'app/drive/components/ShareDialog/ShareDialog'; import StopSharingAndMoveToTrashDialogWrapper from 'views/Trash/components/StopSharingAndMoveToTrashDialogWrapper'; import UploadItemsFailsDialog from 'app/drive/components/UploadItemsFailsDialog/UploadItemsFailsDialog'; import WarningMessageWrapper from 'views/Home/components/WarningMessageWrapper'; import './DriveExplorer.scss'; import { DriveTopBarItems } from './DriveTopBarItems'; +import { ShareDialogWrapper } from 'app/drive/components/ShareDialog/ShareDialogWrapper'; const MenuItemToGetSize = ({ isTrash, @@ -532,7 +532,7 @@ const DriveExplorer = (props: DriveExplorerProps): JSX.Element => { > - + {isShareDialogOpen && ( - actionDispatch(setSelectedItems([]))} /> diff --git a/src/views/Shared/components/ShareItemDialog/ShareItemDialog.tsx b/src/views/Shared/components/ShareItemDialog/ShareItemDialog.tsx index beb3c26525..6adb33e431 100644 --- a/src/views/Shared/components/ShareItemDialog/ShareItemDialog.tsx +++ b/src/views/Shared/components/ShareItemDialog/ShareItemDialog.tsx @@ -10,7 +10,7 @@ import { useAppDispatch, useAppSelector } from '../../../../app/store/hooks'; import PasswordInput from 'components/PasswordInput'; import { Check, Copy } from '@phosphor-icons/react'; import dateService from 'services/date.service'; -import shareService, { copyTextToClipboard } from '../../../../app/share/services/share.service'; +import shareService from '../../../../app/share/services/share.service'; import localStorageService from 'services/local-storage.service'; import { ShareLink } from '@internxt/sdk/dist/drive/share/types'; import { TFunction } from 'i18next'; @@ -18,6 +18,7 @@ import { useTranslationContext } from '../../../../app/i18n/provider/Translation import { domainManager } from '../../../../app/share/services/DomainManager'; import _ from 'lodash'; import { AdvancedSharedItem } from '../../../../app/share/types'; +import { copyTextToClipboard } from 'utils/copyToClipboard.utils'; interface ShareItemDialogProps { share?: ShareLink; diff --git a/test/e2e/tests/pages/drivePage.ts b/test/e2e/tests/pages/drivePage.ts index 0c00806cf3..d57f64a268 100644 --- a/test/e2e/tests/pages/drivePage.ts +++ b/test/e2e/tests/pages/drivePage.ts @@ -1,4 +1,4 @@ -import { test, expect, Locator, Page } from '@playwright/test'; +import { expect, Locator, Page } from '@playwright/test'; export class DrivePage { private page: Page; diff --git a/test/e2e/tests/pages/loginPage.ts b/test/e2e/tests/pages/loginPage.ts index 758891a6bd..a1944b5679 100644 --- a/test/e2e/tests/pages/loginPage.ts +++ b/test/e2e/tests/pages/loginPage.ts @@ -20,8 +20,6 @@ export class LoginPage { private accountRecoveryTitle: Locator; //SignUp private createAccounTitle: Locator; - //terms and conditions - private termsOfService: Locator; constructor(page: Page) { this.page = page; diff --git a/test/e2e/tests/specs/internxt-login.spec.ts b/test/e2e/tests/specs/internxt-login.spec.ts index 9a081fab9e..aa26d122e8 100644 --- a/test/e2e/tests/specs/internxt-login.spec.ts +++ b/test/e2e/tests/specs/internxt-login.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@playwright/test'; +import { expect, Request, Route, test } from '@playwright/test'; import { staticData } from '../helper/staticData'; import { LoginPage } from '../pages/loginPage'; import { getLoggedUser, getUserCredentials } from '../helper/getUser'; @@ -8,7 +8,7 @@ const credentialsFile = getUserCredentials(); const user = getLoggedUser(); const invalidEmail = 'invalid@internxt.com'; -const mockLoginCall = async (route, request) => { +const mockLoginCall = async (route: Route, request: Request) => { await route.fulfill({ status: 200, contentType: 'application/json', @@ -22,7 +22,7 @@ const mockLoginCall = async (route, request) => { }); }; -const mockAccessCall = async (route, request) => { +const mockAccessCall = async (route: Route, request: Request) => { const { email } = request.postDataJSON(); if (invalidEmail === email) { diff --git a/test/e2e/tests/specs/internxt-signup.spec.ts b/test/e2e/tests/specs/internxt-signup.spec.ts index 1cb10be93f..8a17fca331 100644 --- a/test/e2e/tests/specs/internxt-signup.spec.ts +++ b/test/e2e/tests/specs/internxt-signup.spec.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import { expect, test } from '@playwright/test'; +import { expect, Request, Route, test } from '@playwright/test'; import { staticData } from '../helper/staticData'; import { SignUpPage } from '../pages/signUpPage'; import { getUser, getUserCredentials } from '../helper/getUser'; @@ -9,7 +9,7 @@ const credentialsFile = getUserCredentials(); const user = getUser(); const invalidEmail = 'invalid@internxt.com'; -const mockedCall = async (route, request) => { +const mockedCall = async (route: Route, request: Request) => { const { email } = request.postDataJSON(); if (invalidEmail === email) {