From 27b1b4bb4fed8e1e52892715b05fa8d05421e6f5 Mon Sep 17 00:00:00 2001 From: Xavier Abad Date: Thu, 22 Jan 2026 11:46:31 +0100 Subject: [PATCH] refactor(share): extract util functions on a separated file --- .../components/ShareDialog/ShareDialog.tsx | 39 +--- .../ShareDialog/utils/index.test.ts | 210 ++++++++++++++++++ .../components/ShareDialog/utils/index.ts | 45 ++++ 3 files changed, 256 insertions(+), 38 deletions(-) create mode 100644 src/app/drive/components/ShareDialog/utils/index.test.ts create mode 100644 src/app/drive/components/ShareDialog/utils/index.ts diff --git a/src/app/drive/components/ShareDialog/ShareDialog.tsx b/src/app/drive/components/ShareDialog/ShareDialog.tsx index 0f411c548..1a7a6d099 100644 --- a/src/app/drive/components/ShareDialog/ShareDialog.tsx +++ b/src/app/drive/components/ShareDialog/ShareDialog.tsx @@ -2,7 +2,6 @@ import { SharingMeta } from '@internxt/sdk/dist/drive/share/types'; import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; import { UserPlus } from '@phosphor-icons/react'; import { useTranslationContext } from 'app/i18n/provider/TranslationProvider'; -import { MAX_SHARED_NAME_LENGTH } from '../../../../views/Shared/SharedView'; import { Button, Modal } from '@internxt/ui'; import { RootState } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/store/hooks'; @@ -11,14 +10,12 @@ import { uiActions } from 'app/store/slices/ui'; import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react'; import { connect } from 'react-redux'; import errorService from 'services/error.service'; -import localStorageService from 'services/local-storage.service'; import notificationsService, { ToastType } from 'app/notifications/services/notifications.service'; import shareService, { copyTextToClipboard, 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 { DriveItemData } from 'app/drive/types'; import ShareInviteDialog from '../ShareInviteDialog/ShareInviteDialog'; import './ShareDialog.scss'; import envService from 'services/env.service'; @@ -33,14 +30,7 @@ import { ProtectWithPassword } from './components/GeneralView/ProtectWithPasswor import { UserRoleSelection } from './components/GeneralView/UserRoleSelection'; import { InvitedUsersList } from './components/GeneralView/InvitedUsersList'; import { Header } from './components/Header'; - -const cropSharedName = (name: string) => { - if (name.length > MAX_SHARED_NAME_LENGTH) { - return name.substring(0, 32).concat('...'); - } else { - return name; - } -}; +import { cropSharedName, filterEditorAndReader, getLocalUserData, isAdvancedShareItem } from './utils'; type ShareDialogProps = { user: UserSettings; @@ -50,33 +40,6 @@ type ShareDialogProps = { onCloseDialog?: () => void; }; -const isAdvancedShareItem = (item: DriveItemData | AdvancedSharedItem): item is AdvancedSharedItem => { - return item['encryptionKey']; -}; - -const getLocalUserData = () => { - const user = localStorageService.getUser() as UserSettings; - const ownerData = { - name: user.name, - lastname: user.lastname, - email: user.email, - sharingId: '', - avatar: user.avatar, - uuid: user.uuid, - role: { - id: 'NONE', - name: 'OWNER', - createdAt: '', - updatedAt: '', - }, - }; - return ownerData; -}; - -const filterEditorAndReader = (users: Role[]): Role[] => { - return users.filter((user) => user.name === 'EDITOR' || user.name === 'READER'); -}; - const ShareDialog = (props: ShareDialogProps): JSX.Element => { const { onCloseDialog } = props; diff --git a/src/app/drive/components/ShareDialog/utils/index.test.ts b/src/app/drive/components/ShareDialog/utils/index.test.ts new file mode 100644 index 000000000..7a923ffab --- /dev/null +++ b/src/app/drive/components/ShareDialog/utils/index.test.ts @@ -0,0 +1,210 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; +import { isRequestPending, cropSharedName, isAdvancedShareItem, getLocalUserData, filterEditorAndReader } from '.'; +import { REQUEST_STATUS } from '../types'; +import { localStorageService } from 'services'; +import { DriveItemData } from 'app/drive/types'; +import { AdvancedSharedItem } from 'app/share/types'; +import { Role } from '@internxt/sdk/dist/drive/share/types'; + +describe('Check if the request is pending', () => { + test('When request status is pending, then returns true', () => { + const status = REQUEST_STATUS.PENDING; + + const result = isRequestPending(status); + + expect(result).toBeTruthy(); + }); + + test('When request status is accepted, then returns false', () => { + const status = REQUEST_STATUS.ACCEPTED; + + const result = isRequestPending(status); + + expect(result).toBeFalsy(); + }); + + test('When request status is denied, then returns false', () => { + const status = REQUEST_STATUS.DENIED; + + const result = isRequestPending(status); + + expect(result).toBeFalsy(); + }); +}); + +describe('Cropping the name', () => { + test('When name length is less than max length, then returns original name', () => { + const name = 'Short Name'; + + const result = cropSharedName(name); + + expect(result).toBe('Short Name'); + }); + + test('When name length equals max length (32), then returns original name', () => { + const name = 'a'.repeat(32); + + const result = cropSharedName(name); + + expect(result).toBe(name); + }); + + test('When name length exceeds max length, then returns cropped name with ellipsis', () => { + const name = 'This is a very long name that exceeds the maximum allowed length'; + + const result = cropSharedName(name); + + expect(result).not.toBe(name); + expect(result.endsWith('...')).toBe(true); + expect(result.length).toBeLessThan(name.length); + expect(result.startsWith(name.substring(0, 10))).toBe(true); + }); + + test('When name is exactly 33 characters, then crops to 32 characters plus ellipsis', () => { + const name = 'a'.repeat(33); + + const result = cropSharedName(name); + + expect(result).not.toBe(name); + expect(result.endsWith('...')).toBe(true); + expect(result.length).toBe(35); + expect(result.startsWith('a'.repeat(32))).toBe(true); + }); +}); + +describe('Check if it is an advanced share item', () => { + test('When item has an encryption key property, then returns truthy value', () => { + const item = { + id: '123', + encryptionKey: 'some-key', + name: 'Test Item', + } as unknown as AdvancedSharedItem; + + const result = isAdvancedShareItem(item); + + expect(result).toBeTruthy(); + }); + + test('When item does not have an encryption key property, then returns falsy value', () => { + const item = { + id: '123', + name: 'Test Item', + } as unknown as DriveItemData; + + const result = isAdvancedShareItem(item); + + expect(result).toBeFalsy(); + }); + + test('When item has an encryption key with undefined value, then returns falsy value', () => { + const item = { + id: '123', + encryptionKey: undefined, + name: 'Test Item', + } as any; + + const result = isAdvancedShareItem(item); + + expect(result).toBeFalsy(); + }); +}); + +describe('Get the local user data', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test('When user data exists in local storage, then returns formatted owner data', () => { + const mockUser = { + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + avatar: 'avatar-url', + uuid: 'user-uuid-123', + } as any; + vi.spyOn(localStorageService, 'getUser').mockReturnValue(mockUser); + + const result = getLocalUserData(); + + expect(result).toEqual({ + name: 'John', + lastname: 'Doe', + email: 'john@example.com', + sharingId: '', + avatar: 'avatar-url', + uuid: 'user-uuid-123', + role: { + id: 'NONE', + name: 'OWNER', + createdAt: '', + updatedAt: '', + }, + }); + }); + + test('When user has no avatar, then returns owner data with null avatar', () => { + const mockUser = { + name: 'Bob', + lastname: 'Johnson', + email: 'bob@example.com', + avatar: null, + uuid: 'user-uuid-789', + } as any; + vi.spyOn(localStorageService, 'getUser').mockReturnValue(mockUser); + + const result = getLocalUserData(); + + expect(result.avatar).toBeNull(); + }); +}); + +describe('Filtering roles by EDITOR and READER', () => { + test('When roles array contains only EDITOR and READER, then returns all roles', () => { + const roles: Role[] = [ + { id: '1', name: 'EDITOR', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + { id: '2', name: 'READER', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + ]; + + const result = filterEditorAndReader(roles); + + expect(result).toHaveLength(2); + expect(result).toEqual(roles); + }); + + test('When roles array contains OWNER, then filters it out', () => { + const roles: Role[] = [ + { 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 result = filterEditorAndReader(roles); + + expect(result).toHaveLength(2); + expect(result.every((role) => role.name === 'EDITOR' || role.name === 'READER')).toBe(true); + }); + + test('When roles array is empty, then returns empty array', () => { + const roles: Role[] = []; + + const result = filterEditorAndReader(roles); + + expect(result).toHaveLength(0); + expect(result).toEqual([]); + }); + + test('When roles array contains no EDITOR or READER, then returns empty array', () => { + const roles: Role[] = [ + { id: '1', name: 'OWNER', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + { id: '2', name: 'ADMIN', createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01') }, + ]; + + const result = filterEditorAndReader(roles); + + expect(result).toHaveLength(0); + }); +}); diff --git a/src/app/drive/components/ShareDialog/utils/index.ts b/src/app/drive/components/ShareDialog/utils/index.ts new file mode 100644 index 000000000..e531c6c0c --- /dev/null +++ b/src/app/drive/components/ShareDialog/utils/index.ts @@ -0,0 +1,45 @@ +import { DriveItemData } from 'app/drive/types'; +import { REQUEST_STATUS, RequestStatus } from '../types'; +import { AdvancedSharedItem } from 'app/share/types'; +import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings'; +import { Role } from '@internxt/sdk/dist/drive/share/types'; +import { MAX_SHARED_NAME_LENGTH } from 'views/Shared/SharedView'; +import localStorageService from 'services/local-storage.service'; + +export const isRequestPending = (status: RequestStatus): boolean => + status !== REQUEST_STATUS.DENIED && status !== REQUEST_STATUS.ACCEPTED; + +export const cropSharedName = (name: string) => { + if (name.length > MAX_SHARED_NAME_LENGTH) { + return name.substring(0, 32).concat('...'); + } else { + return name; + } +}; + +export const isAdvancedShareItem = (item: DriveItemData | AdvancedSharedItem): item is AdvancedSharedItem => { + return item['encryptionKey']; +}; + +export const getLocalUserData = () => { + const user = localStorageService.getUser() as UserSettings; + const ownerData = { + name: user.name, + lastname: user.lastname, + email: user.email, + sharingId: '', + avatar: user.avatar, + uuid: user.uuid, + role: { + id: 'NONE', + name: 'OWNER', + createdAt: '', + updatedAt: '', + }, + }; + return ownerData; +}; + +export const filterEditorAndReader = (users: Role[]): Role[] => { + return users.filter((user) => user.name === 'EDITOR' || user.name === 'READER'); +};