diff --git a/src/App.tsx b/src/App.tsx index a7bf98e..d3a1372 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import './App.css'; import ThemeHandler from './components/common/handler/ThemeHandler'; import Navbar from './components/pegase/navbar/Navbar'; import PegaseStar from './components/pegase/star/PegaseStar'; -import { UserContext } from './contexts/UserContext'; +import { UserContext } from '@/store/contexts/UserContext'; import { PEGASE_NAVBAR_ID } from './shared/constants'; import { THEME_COLOR } from './shared/types'; import { menuBottomData, menuTopData } from './routes'; diff --git a/src/components/common/data/stdTable/TableCore.tsx b/src/components/common/data/stdTable/TableCore.tsx index 3af18b1..5eaa8e5 100644 --- a/src/components/common/data/stdTable/TableCore.tsx +++ b/src/components/common/data/stdTable/TableCore.tsx @@ -4,10 +4,10 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useStdId } from '@/hooks/useStdId'; import { Cell, flexRender, Header, Row, Table } from '@tanstack/react-table'; import { clsx } from 'clsx'; import { tableCoreRowClassBuilder } from './tableCoreRowClassBuilder'; +import { useRdsId } from 'rte-design-system-react'; export type ColumnSizeType = 'pixels' | 'meta'; export type ColumnResizeMode = 'onChange' | 'onEnd'; @@ -97,7 +97,7 @@ const tableStyleBuilder = (table: Table, columnSize: ColumnSizeTy : undefined; const TableCore = ({ table, id: propId, striped, trClassName, columnSize = 'meta' }: TableCoreProps) => { - const id = useStdId('table-', propId); + const id = useRdsId('table-', propId); const handleToggleRow = (row: Row) => () => { if (row.getCanSelect()) { diff --git a/src/components/common/handler/ThemeHandler.tsx b/src/components/common/handler/ThemeHandler.tsx index 2c14881..3c30596 100644 --- a/src/components/common/handler/ThemeHandler.tsx +++ b/src/components/common/handler/ThemeHandler.tsx @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { UserContext } from '@/contexts/UserContext'; +import { UserContext } from '@/store/contexts/UserContext'; import usePrevious from '@/hooks/common/usePrevious'; import { THEME_COLOR } from '@/shared/types'; import { useEffect } from 'react'; diff --git a/src/components/common/handler/test/ThemeHandler.test.tsx b/src/components/common/handler/test/ThemeHandler.test.tsx index 711baf8..c53ba63 100644 --- a/src/components/common/handler/test/ThemeHandler.test.tsx +++ b/src/components/common/handler/test/ThemeHandler.test.tsx @@ -5,14 +5,14 @@ */ import { render } from '@testing-library/react'; -import { describe, it, expect, vi, Mock } from 'vitest'; -import { UserContext } from '@/contexts/UserContext'; -import usePrevious from '@/hooks/common/usePrevious'; +import { describe, expect, it, Mock, vi } from 'vitest'; import { THEME_COLOR } from '@/shared/types'; import ThemeHandler from '../ThemeHandler'; +import { UserContext } from '@/store/contexts/UserContext'; +import usePrevious from '@/hooks/common/usePrevious'; // Mocking the UserContext and usePrevious hook -vi.mock('@/contexts/UserContext', () => ({ +vi.mock('@/store/contexts/UserContext', () => ({ UserContext: { useStore: vi.fn(), }, diff --git a/src/components/common/layout/stdAvatar/StdAvatar.tsx b/src/components/common/layout/stdAvatar/StdAvatar.tsx index 8e692c6..1c0ac7b 100644 --- a/src/components/common/layout/stdAvatar/StdAvatar.tsx +++ b/src/components/common/layout/stdAvatar/StdAvatar.tsx @@ -6,8 +6,7 @@ import { avatarClassBuilder } from './avatarClassBuilder'; import { AVATAR_COLORS } from '../stdAvatarGroup/avatarTools'; -import { useStdId } from '@/hooks/common/useStdId'; -import { RdsTextTooltip } from 'rte-design-system-react'; +import { RdsTextTooltip, useRdsId } from 'rte-design-system-react'; type StdAvatarProps = { initials: string; @@ -24,7 +23,7 @@ const OFFSET_HOVER_HEIGHT = 5; const StdAvatar = ({ initials, size, backgroundColor, fullname, id: propsId }: StdAvatarProps) => { const avatarClasses = avatarClassBuilder(size, backgroundColor); - const id = useStdId('avatar', propsId); + const id = useRdsId('avatar', propsId); return (
diff --git a/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx b/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx index 1c18f57..5981c4a 100644 --- a/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx +++ b/src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx @@ -9,11 +9,11 @@ import StdAvatar from '../stdAvatar/StdAvatar'; import { AvatarSize } from '../stdAvatar/StdAvatar'; import { classBuilder as groupContainerClassBuilder } from './avatarGroupClassBuilder'; import { getColor, getUserFullname, getUserInitials, splitUserList } from './avatarTools'; -import { useStdId } from '@/hooks/common/useStdId'; +import { useRdsId } from 'rte-design-system-react'; type StdAvatarGroup = { users: User[]; - avatarSize?: AvatarSize; + avatarSize: AvatarSize; id?: string; }; @@ -21,7 +21,7 @@ const StdAvatarGroup = ({ users: listUser, avatarSize = 'es', id: propsId }: Std const groupContainerClasses = groupContainerClassBuilder(avatarSize); const users = splitUserList(listUser); - const id = useStdId('avatarGroup', propsId); + const id = useRdsId('avatarGroup', propsId); return (
diff --git a/src/components/common/layout/stdNavbar/StdNavbar.tsx b/src/components/common/layout/stdNavbar/StdNavbar.tsx index db145e1..2ea905c 100644 --- a/src/components/common/layout/stdNavbar/StdNavbar.tsx +++ b/src/components/common/layout/stdNavbar/StdNavbar.tsx @@ -4,7 +4,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useStdId } from '@/hooks/common/useStdId'; import { MenuNavItem } from '@/shared/types'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,7 +11,7 @@ import StdNavbarController from './StdNavbarController'; import StdNavbarHeader from './StdNavbarHeader'; import StdNavbarMenu from './StdNavbarMenu'; import { navbarClassBuilder } from './navbarClassBuilder'; -import { RdsDivider } from 'rte-design-system-react'; +import { RdsDivider, useRdsId } from 'rte-design-system-react'; export type StdNavbarProps = { topItems: MenuNavItem[]; @@ -32,7 +31,7 @@ const StdNavbar = ({ topItems, bottomItems, appName, appVersion, headerLink, id: }; const navbarClasses = navbarClassBuilder(expanded); - const id = useStdId('navbar', propsId); + const id = useRdsId('navbar', propsId); const controllerId = `${id}-controller`; const headerId = `${id}-header`; const controllerLabel = expanded ? t('components.navbar.@minimize') : t('components.navbar.@expand'); diff --git a/src/components/common/layout/stdNavbar/StdNavbarController.tsx b/src/components/common/layout/stdNavbar/StdNavbarController.tsx index 011ea54..beaa174 100644 --- a/src/components/common/layout/stdNavbar/StdNavbarController.tsx +++ b/src/components/common/layout/stdNavbar/StdNavbarController.tsx @@ -4,9 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import StdTextTooltip from '../stdTextTooltip/StdTextTooltip'; import { navbarControllerClassBuilder } from './navbarClassBuilder'; -import { RdsIcon, RdsIconId } from 'rte-design-system-react'; +import { RdsIcon, RdsIconId, RdsTextTooltip } from 'rte-design-system-react'; type StdNavbarControllerProps = { id: string; @@ -17,16 +16,16 @@ type StdNavbarControllerProps = { const StdNavbarController = ({ label, id, action, expanded = true }: StdNavbarControllerProps) => { const iconId = expanded ? RdsIconId.KeyboardDoubleArrowLeft : RdsIconId.KeyboardDoubleArrowRight; - const navbarControllerClasses = navbarControllerClassBuilder(expanded); + const navbarControllerClasses = expanded ? navbarControllerClassBuilder(expanded) : undefined; return (
- +
{expanded ? : } {expanded && <>{label}}
-
+
); }; diff --git a/src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx b/src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx index fec7781..ebac153 100644 --- a/src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx +++ b/src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx @@ -4,13 +4,11 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useStdId } from '@/hooks/common/useStdId'; import { MenuNavItem } from '@/shared/types'; import { Link } from 'react-router-dom'; -import StdTextTooltip from '../stdTextTooltip/StdTextTooltip'; import { navbarItemClassBuilder } from './navbarClassBuilder'; import { useTranslation } from 'react-i18next'; -import { RdsIcon } from 'rte-design-system-react'; +import { RdsIcon, RdsTextTooltip, useRdsId } from 'rte-design-system-react'; type StdNavbarMenuItemProps = { item: MenuNavItem; @@ -19,18 +17,18 @@ type StdNavbarMenuItemProps = { }; const StdNavbarMenuItem = ({ item, expanded = true, selected = false }: StdNavbarMenuItemProps) => { - const id = useStdId('navbar-item', item.id); + const id = useRdsId('navbar-item', item.id); const { t } = useTranslation(); const { path, key, icon, label } = item; const navbarMenuItemClasses = navbarItemClassBuilder(selected, expanded); return ( - + {expanded && t(label)} - + ); }; diff --git a/src/components/common/layout/stdTextTooltip/StdTextTooltip.tsx b/src/components/common/layout/stdTextTooltip/StdTextTooltip.tsx deleted file mode 100644 index fb3e6ca..0000000 --- a/src/components/common/layout/stdTextTooltip/StdTextTooltip.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { Placement } from '@floating-ui/react'; -import { Dispatch, PropsWithChildren, SetStateAction } from 'react'; -import { RdsFloatingWrapper, RdsTooltip } from 'rte-design-system-react'; - -const { Trigger, Element } = RdsFloatingWrapper; - -export type StdTextTooltipProps = { - text: string; - placement?: Placement; - fallbackPlacement?: Placement[]; - offset?: number; - enabled?: boolean; - show?: boolean; - setShow?: Dispatch>; - disableArrow?: boolean; - id?: string; -}; - -const StdTextTooltip = ({ - text, - placement, - fallbackPlacement, - offset, - enabled = true, - show, - setShow, - disableArrow, - children, - id, -}: PropsWithChildren) => ( - - {children} - {enabled ? ( - - {text} - - ) : ( - <> - )} - -); - -export default StdTextTooltip; diff --git a/src/components/common/layout/stdTextTooltip/tests/stdTooltipText.test.tsx b/src/components/common/layout/stdTextTooltip/tests/stdTooltipText.test.tsx deleted file mode 100644 index 7ac382f..0000000 --- a/src/components/common/layout/stdTextTooltip/tests/stdTooltipText.test.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { render, screen } from '@testing-library/react'; -import StdTextTooltip from '../StdTextTooltip'; - -const TEST_COMPONENT =
; -const TEST_ID = 'tooltip-text-id'; -const TEST_TEXT = 'test-text'; - -describe('StdTextTooltip', () => { - it('render the StdTextTooltip component with expected id, tooltip text and children ', () => { - render( - - {TEST_COMPONENT} - , - ); - - expect(document.querySelector(`#${TEST_ID}`)).toBeInTheDocument(); - expect(screen.getByRole('tooltip').textContent).toBe(TEST_TEXT); - expect(screen.getByRole('article')).toBeInTheDocument(); - }); -}); diff --git a/src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx b/src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx index 4f81bad..0277a8a 100644 --- a/src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx +++ b/src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx @@ -5,15 +5,14 @@ */ import React, { useRef, useState } from 'react'; -import StdTextTooltip from '../stdTextTooltip/StdTextTooltip'; -import { useCallOnResize } from '@/hooks/common/useCallOnResize'; +import { RdsTextTooltip, useCallOnResize } from 'rte-design-system-react'; type StdTextTooltipProps = { text: string; id: string | undefined } & React.HTMLProps; const DEFAULT_OFFSET = 8; const StdTextWithTooltip = ({ text, id, ...props }: StdTextTooltipProps) => { - const spanRef = useRef(null); + const spanRef = useRef(null); const [enabled, setEnabled] = useState(false); useCallOnResize(() => { @@ -24,11 +23,11 @@ const StdTextWithTooltip = ({ text, id, ...props }: StdTextTooltipProps) => { }, spanRef.current?.id); return ( - + {text} - + ); }; diff --git a/src/components/pegase/pegaseCard/pegaseCard.tsx b/src/components/pegase/pegaseCard/pegaseCard.tsx index 13b4cff..3075244 100644 --- a/src/components/pegase/pegaseCard/pegaseCard.tsx +++ b/src/components/pegase/pegaseCard/pegaseCard.tsx @@ -17,7 +17,7 @@ export type PegaseCardTripleActionButtonProps = { }; type PegaseCardTripleActionProps = Omit & - Omit & { + PegaseCardTitleProps & { title: string; buttons?: PegaseCardTripleActionButtonProps; secondaryButtonPosition?: PegaseCardSecondaryButtonPosition; diff --git a/src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx b/src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx index d60ce4a..702593b 100644 --- a/src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx +++ b/src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx @@ -19,11 +19,11 @@ const TEST_SECONDARY_BUTTON: Omit = }; const TEST_CHILDREN =
; const TEST_ID = 'card-triple-action-id'; -const TEST_DROPDOWN_DROPDOWN: RdsDropdownOption[] = [ +const TEST_DROPDOWN_DROPDOWN = [ { key: 'op1', label: 'Option 1', value: 'op1', onItemClick: noop }, { key: 'op2', label: 'Option 2', value: 'op2', onItemClick: noop }, { key: 'op3', label: 'Option 3', value: 'op3', onItemClick: noop }, -]; +] as RdsDropdownOption[]; describe('PegaseCard', () => { it('renders the default PegaseCard component', () => { diff --git a/src/hooks/common/test/useCallOnResize.test.ts b/src/hooks/common/test/useCallOnResize.test.ts deleted file mode 100644 index 609d326..0000000 --- a/src/hooks/common/test/useCallOnResize.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { renderHook } from '@testing-library/react'; -import { useCallOnResize } from '../useCallOnResize'; - -global.ResizeObserver = vi.fn().mockImplementation(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), -})); - -describe('useCallOnResize', () => { - it('callback should be called when scren size change', async () => { - const mock = vi.fn(); - renderHook(() => useCallOnResize(mock)); - global.dispatchEvent(new Event('resize')); - await vi.waitFor(() => expect(mock).toBeCalled()); - }); -}); diff --git a/src/hooks/common/test/useDebounce.test.ts b/src/hooks/common/test/useDebounce.test.ts deleted file mode 100644 index 0337685..0000000 --- a/src/hooks/common/test/useDebounce.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { renderHook, act, waitFor } from '@testing-library/react'; -import useDebounce from '../useDebounce'; - -describe('useDebounce', () => { - test('should call the callback after the delay', () => { - const callback = vi.fn(); - act(() => { - renderHook(() => useDebounce(callback, 'dependency', 200)); - }); - - expect(callback).not.toBeCalled(); - //void waitFor(() => expect(callback).toBeCalled(), { timeout: 300 }); - }); - - test('should reset the timer when the dependency changes', async () => { - const callback = vi.fn(); - let rerender: (props?: string) => void; - act(() => { - const { rerender: rerenderHook } = renderHook((dependency: string) => useDebounce(callback, dependency, 200), { - initialProps: 'initialDependency', - }); - rerender = rerenderHook; - }); - - await waitFor(() => expect(callback).not.toBeCalled()); - act(() => { - rerender?.('updatedDependency'); - }); - await waitFor(() => expect(callback).not.toBeCalled()); - await waitFor(() => expect(callback).toBeCalled()); - }); -}); diff --git a/src/hooks/common/test/useInputFormState.test.ts b/src/hooks/common/test/useInputFormState.test.ts deleted file mode 100644 index cd0f44d..0000000 --- a/src/hooks/common/test/useInputFormState.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { renderHook, act, waitFor } from '@testing-library/react'; -import { useInputFormState } from '../useInputFormState'; - -describe('useInputFormState', () => { - test('should initialize with the provided value', () => { - const { result } = renderHook(() => useInputFormState('initialValue', '', undefined)); - - expect(result.current.value).toBe('initialValue'); - }); - - test('should update the value when onChange is called', () => { - const { result } = renderHook(() => useInputFormState('initialValue', '', undefined)); - - act(() => { - result.current.setValue('newValue'); - }); - - expect(result.current.value).toBe('newValue'); - }); - - test('should update the value when updateRef is called', async () => { - const { result } = renderHook(() => useInputFormState('initialValue', '', undefined, vi.fn())); - - act(() => { - result.current.setValue('newValue'); - }); - - await waitFor(() => expect(result.current.value).toBe('newValue')); - }); - - test('should call onChange when the value changes', async () => { - const onChange = vi.fn(); - const { result } = renderHook(() => useInputFormState('initialValue', '', onChange)); - - act(() => { - result.current.setValue('newValue'); - }); - - await waitFor(() => expect(onChange).toHaveBeenCalledWith('newValue')); - }); -}); diff --git a/src/hooks/common/test/usePrevious.test.ts b/src/hooks/common/test/usePrevious.test.ts index 1bbe447..2e12ba5 100644 --- a/src/hooks/common/test/usePrevious.test.ts +++ b/src/hooks/common/test/usePrevious.test.ts @@ -4,10 +4,12 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { act, renderHook, waitFor } from '@testing-library/react'; +import { act, Queries, renderHook, RenderHookOptions, waitFor } from '@testing-library/react'; import usePrevious from '../usePrevious'; describe('usePrevious', () => { + const mockHTMLElement = document.createElement('div'); + it('should return undefined as previous value when no value is provided', () => { const { result } = renderHook(() => usePrevious(undefined, undefined)); expect(result.current).toBeUndefined(); @@ -19,31 +21,35 @@ describe('usePrevious', () => { }); it('should return previous value when a new value is provided', async () => { - const { result, rerender } = renderHook((value) => usePrevious(value, 'initial'), { - initialProps: 'initial', - }); + const { result, rerender } = renderHook((value) => usePrevious(value, mockHTMLElement), { + initialProps: mockHTMLElement, + } as RenderHookOptions); act(() => { - rerender('updated'); + rerender(document.createElement('div')); }); - expect(result.current).toBe('initial'); + expect(result.current).toBe(mockHTMLElement); - await waitFor(() => expect(result.current).not.toBe('updated')); + await waitFor(() => expect(result.current).not.toBe(document.createElement('div'))); }); it('should return updated previous value when a new value is provided', async () => { - const { result, rerender } = renderHook((value) => usePrevious(value, 'initial'), { - initialProps: 'initial', - }); + const { result, rerender } = renderHook((value) => usePrevious(value, mockHTMLElement), { + initialProps: mockHTMLElement, + } as RenderHookOptions); + + const mockHTMLElementUpdate = document.createElement('div'); act(() => { - rerender('updated'); + rerender(mockHTMLElementUpdate); }); + const mockHTMLElementUpdate2 = document.createElement('div'); + act(() => { - rerender('updated 2'); + rerender(mockHTMLElementUpdate2); }); - await waitFor(() => expect(result.current).toBe('updated')); + await waitFor(() => expect(result.current).toBe(mockHTMLElementUpdate)); }); }); diff --git a/src/hooks/common/test/useStdId.test.ts b/src/hooks/common/test/useStdId.test.ts deleted file mode 100644 index aeda548..0000000 --- a/src/hooks/common/test/useStdId.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { renderHook } from '@testing-library/react'; -import { useStdId } from '../useStdId'; - -test('useStdId should return a unique ID with the given prefix', () => { - const { result } = renderHook(() => useStdId('prefix')); - const id = result.current; - - expect(id).toMatch(/^prefix-.*$/); -}); - -test('useStdId should return the provided ID if it is valid', () => { - const { result } = renderHook(() => useStdId('prefix', 'custom-id')); - const id = result.current; - - expect(id).toBe('custom-id'); -}); - -test('useStdId should return a unique ID if the provided ID is invalid', () => { - const { result } = renderHook(() => useStdId('prefix', '')); - const id = result.current; - - expect(id).toMatch(/^prefix-.*$/); -}); diff --git a/src/hooks/common/test/useTimeout.test.ts b/src/hooks/common/test/useTimeout.test.ts deleted file mode 100644 index 2cc32da..0000000 --- a/src/hooks/common/test/useTimeout.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { renderHook, act } from '@testing-library/react'; -import useTimeout from '../useTimeout'; - -vi.useFakeTimers(); - -describe('useTimeout', () => { - test('should call the callback after the specified delay', () => { - const callback = vi.fn(); - renderHook(() => useTimeout(callback, 1000)); - - act(() => { - vi.advanceTimersByTime(1000); - }); - - expect(callback).toHaveBeenCalled(); - }); - - test('should reset the timer when reset function is called', () => { - const callback = vi.fn(); - const { result } = renderHook(() => useTimeout(callback, 1000)); - - act(() => { - vi.advanceTimersByTime(500); - result.current[0](); - vi.advanceTimersByTime(500); - }); - - expect(callback).not.toHaveBeenCalled(); - - act(() => { - vi.advanceTimersByTime(500); - }); - - expect(callback).toHaveBeenCalled(); - }); - - test('should clear the timer when clear function is called', () => { - const callback = vi.fn(); - const { result } = renderHook(() => useTimeout(callback, 1000)); - - act(() => { - vi.advanceTimersByTime(500); - result.current[1](); - vi.advanceTimersByTime(1000); - }); - - expect(callback).not.toHaveBeenCalled(); - }); -}); diff --git a/src/hooks/common/useActiveKeyboard.ts b/src/hooks/common/useActiveKeyboard.ts deleted file mode 100644 index 823c7e9..0000000 --- a/src/hooks/common/useActiveKeyboard.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { KeyboardEvent, useRef, useState } from 'react'; - -export const SPACEBAR_INPUT = 'Space'; - -type OptionsActiveKeyboard = { - id?: string; - interactiveKeyCodes?: string[]; -}; - -const useActiveKeyboard = ( - handlerKeyup: (e: KeyboardEvent) => void, - options: OptionsActiveKeyboard = {}, -) => { - const { id, interactiveKeyCodes } = options; - const [isActiveKeyboard, setIsActiveKeyboard] = useState(false); - const interactiveKeysRef = useRef(interactiveKeyCodes ?? [SPACEBAR_INPUT]); - - const onKeyDown = (e: React.KeyboardEvent) => { - if (interactiveKeysRef.current?.includes(e.code) && (!id || (e.target as T).id === id)) { - e.preventDefault(); - setIsActiveKeyboard(true); - } - }; - - const onKeyUp = (e: React.KeyboardEvent) => { - if (interactiveKeysRef.current?.includes(e.code) && (!id || (e.target as T).id === id)) { - handlerKeyup(e); - setIsActiveKeyboard(false); - } - }; - - const onBlur = () => { - setIsActiveKeyboard(false); - }; - - return [{ onKeyDown, onKeyUp, onBlur }, isActiveKeyboard] as [ - { - onKeyDown: (e: React.KeyboardEvent) => void; - onKeyUp: (e: React.KeyboardEvent) => void; - onBlur: () => void; - }, - boolean, - ]; -}; - -export default useActiveKeyboard; diff --git a/src/hooks/common/useCallOnResize.ts b/src/hooks/common/useCallOnResize.ts deleted file mode 100644 index 8989d62..0000000 --- a/src/hooks/common/useCallOnResize.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useCallback, useEffect, useRef } from 'react'; - -export function useCallOnResize(callback: () => void, elementId?: string, debounceTime = 200) { - const timer = useRef>(); - const callbackRef = useRef(() => { - callback(); - }); - - const handleResize = useCallback(() => { - clearTimeout(timer.current); - timer.current = setTimeout(callbackRef.current, debounceTime); - }, [debounceTime]); - - useEffect(() => { - const resizeObserver = new ResizeObserver(handleResize); - if (elementId) { - const element = document.getElementById(elementId); - element && resizeObserver.observe(element); - } else { - window.addEventListener('resize', handleResize); - } - handleResize(); - return () => { - if (elementId) { - resizeObserver.disconnect(); - } else { - window.removeEventListener('resize', handleResize); - } - }; - }, [handleResize, elementId]); -} diff --git a/src/hooks/common/useDebounce.ts b/src/hooks/common/useDebounce.ts deleted file mode 100644 index 1601a92..0000000 --- a/src/hooks/common/useDebounce.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useEffect, useRef } from 'react'; -import useTimeout from './useTimeout'; - -/** - * Hook that call the callback after a delay and reset timer when dependencies are updated - * - * @usage send update when filling input, it will call the update but not while typing. - * @example useDebounce(onChange,dep1,1000) - * @param callback function to call after the timer - * @param whatchingDependency dependency that reset the timer - * @param delay delay in ms - */ -const useDebounce = (callback: () => void | Promise, whatchingDependency: unknown, delay = 200) => { - const callbackRef = useRef(callback); - const [reset, clear] = useTimeout(() => void callbackRef.current(), delay); - - useEffect(() => { - reset(); - }, [whatchingDependency, reset]); - - useEffect(() => { - callbackRef.current = callback; - }, [whatchingDependency, callback]); - useEffect(clear, [clear]); -}; - -export default useDebounce; diff --git a/src/hooks/common/useInputFormState.ts b/src/hooks/common/useInputFormState.ts deleted file mode 100644 index 937c0e7..0000000 --- a/src/hooks/common/useInputFormState.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { StdChangeHandler } from '@/shared/types'; -import { useEffect, useRef, useState } from 'react'; -import useDebounce from './useDebounce'; - -export const useInputFormState = ( - value: TValue, - defaultValue: TValue, - onChange: StdChangeHandler | undefined, - updateRef?: (value: TValue) => void, -) => { - const [stateValue, setStateValue] = useState(defaultValue || value); - const updateRefRef = useRef(updateRef); - - const renderCount = useRef(0); - renderCount.current += 1; - useEffect(() => { - if (renderCount.current > 1) { - setStateValue(value); - updateRefRef.current?.(value); - } - }, [value]); - - useEffect(() => { - updateRefRef.current?.(stateValue); - }, [stateValue]); - - useDebounce(async () => await onChange?.(stateValue ?? defaultValue), stateValue); - - return { value: stateValue, setValue: setStateValue }; -}; diff --git a/src/hooks/common/useTimeout.ts b/src/hooks/common/useTimeout.ts deleted file mode 100644 index d146d5e..0000000 --- a/src/hooks/common/useTimeout.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useCallback, useEffect, useRef } from 'react'; - -/** - * hook to call a function after a delay and provide reset and cancel options - * @param callback function called after a delay - * @param delay delay in ms (default 1000 ms) - * @returns [reset, clear] - * - * reset => restart the timer - * - * clear => wipe the timer (do not call the callback) - * - * @example - * const [name,setName] = useState("") - * const [reset , clear] = useTimeout(()=>setName("toto"),1000) - */ -const useTimeout = (callback: () => void, delay = 1000) => { - // useRef prevent multiple useEffect triggering with function dependency - const callbackRef = useRef(callback); - const timeoutRef = useRef(); - - const set = useCallback(() => { - timeoutRef.current = setTimeout(() => callbackRef.current(), delay); - }, [delay]); - - const clear = useCallback(() => { - timeoutRef.current && clearTimeout(timeoutRef.current); - }, []); - - useEffect(() => { - set(); - return clear; - }, [delay, set, clear]); - - const reset = useCallback(() => { - clear(); - set(); - }, [clear, set]); - - return [reset, clear]; -}; - -export default useTimeout; diff --git a/src/hooks/test/useDropdownOptions.test.ts b/src/hooks/test/useDropdownOptions.test.ts new file mode 100644 index 0000000..e4650dd --- /dev/null +++ b/src/hooks/test/useDropdownOptions.test.ts @@ -0,0 +1,124 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { renderHook } from '@testing-library/react'; +import { NO_WRAP_CLASS, useDropdownOptions } from '@/hooks/useDropdownOptions'; +import { describe, expectTypeOf, it } from 'vitest'; +import { RdsDropdownOption, RdsIconId } from 'rte-design-system-react'; + +describe('useDropdownOptions', () => { + const mockOnClick = vi.fn(); + + it('should return settingOption, deleteOption and pinOption functions', () => { + const { result } = renderHook(() => useDropdownOptions()); + + expectTypeOf(result.current.settingOption).toBeFunction(); + expectTypeOf(result.current.settingOption).returns.toEqualTypeOf(); + expectTypeOf(result.current.deleteOption).toBeFunction(); + expectTypeOf(result.current.deleteOption).returns.toEqualTypeOf(); + expectTypeOf(result.current.pinOption).toBeFunction(); + expectTypeOf(result.current.pinOption).returns.toEqualTypeOf(); + }); + + it('should call settingOption and return the right set of options', () => { + const { result } = renderHook(() => useDropdownOptions()); + const settingOptions = { + key: 'setting', + label: 'Setting', + value: 'setting', + onItemClick: mockOnClick, + disabled: undefined, + icon: RdsIconId.Settings, + extraClasses: NO_WRAP_CLASS, + } as RdsDropdownOption; + + expect(result.current.settingOption(mockOnClick)).toEqual(settingOptions); + expect(result.current.settingOption(mockOnClick, 'noSettings')).toEqual({ + key: 'setting', + label: 'noSettings', + value: 'setting', + onItemClick: mockOnClick, + disabled: undefined, + icon: RdsIconId.Settings, + extraClasses: NO_WRAP_CLASS, + }); + expect(result.current.settingOption(mockOnClick)).toEqual({ + key: 'setting', + label: 'Setting', + value: 'setting', + onItemClick: mockOnClick, + disabled: undefined, + icon: RdsIconId.Settings, + extraClasses: NO_WRAP_CLASS, + }); + expect(result.current.settingOption(mockOnClick, 'settings', true)).toEqual({ + key: 'setting', + label: 'settings', + value: 'setting', + onItemClick: mockOnClick, + disabled: true, + icon: RdsIconId.Settings, + extraClasses: NO_WRAP_CLASS, + }); + }); + + it('should call deleteOption and return the right set of options', () => { + const { result } = renderHook(() => useDropdownOptions()); + const classes = 'whitespace-nowrap [&]:text-error-600 [&]:hover:text-error-600'; + + const deleteOptions = { + key: 'delete', + label: 'Delete', + value: 'delete', + onItemClick: mockOnClick, + disabled: undefined, + icon: RdsIconId.Delete, + extraClasses: classes, + } as RdsDropdownOption; + + expect(result.current.deleteOption(mockOnClick)).toEqual(deleteOptions); + expect(result.current.deleteOption(mockOnClick, 'deleteLabel')).toEqual({ + key: 'delete', + label: 'deleteLabel', + value: 'delete', + onItemClick: mockOnClick, + disabled: undefined, + icon: RdsIconId.Delete, + extraClasses: classes, + } as RdsDropdownOption); + expect(result.current.deleteOption(mockOnClick, undefined, false)).toEqual({ + key: 'delete', + label: 'Delete', + value: 'delete', + onItemClick: mockOnClick, + disabled: false, + icon: RdsIconId.Delete, + extraClasses: classes, + } as RdsDropdownOption); + }); + + it('should call pinOption and return the right set of options', () => { + const { result } = renderHook(() => useDropdownOptions()); + const pinOptions = { + key: 'pin', + label: 'Unpin', + value: 'pin', + onItemClick: mockOnClick, + icon: RdsIconId.KeepOff, + extraClasses: NO_WRAP_CLASS, + } as RdsDropdownOption; + + expect(result.current.pinOption(true, mockOnClick)).toEqual(pinOptions); + expect(result.current.pinOption(false, mockOnClick)).toEqual({ + key: 'pin', + label: 'Pin', + value: 'pin', + onItemClick: mockOnClick, + icon: RdsIconId.PushPin, + extraClasses: NO_WRAP_CLASS, + } as RdsDropdownOption); + }); +}); diff --git a/src/hooks/test/useFetchProjectList.test.ts b/src/hooks/test/useFetchProjectList.test.ts new file mode 100644 index 0000000..fbe652e --- /dev/null +++ b/src/hooks/test/useFetchProjectList.test.ts @@ -0,0 +1,74 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { renderHook, waitFor } from '@testing-library/react'; +import { useFetchProjectList } from '@/hooks/useFetchProjectList.ts'; + +const mockFetch = vi.fn(); +global.fetch = mockFetch; +vi.mock('@/shared/notification/notification'); +vi.mock('@/envVariables', () => ({ + getEnvVariables: vi.fn(() => 'https://mockapi.com'), +})); + +describe('useFetchProjectList', () => { + beforeEach(() => { + global.fetch = vi.fn(() => + Promise.resolve({ + json: () => + Promise.resolve({ + content: [ + { + projectId: '1', + name: 'Project 1', + tags: ['Tag1', 'Tag2'], + creationDate: '2023-10-01', + createdBy: 'User A', + }, + ], + totalElements: 1, + }), + }), + ) as unknown as typeof fetch; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('fetches projects on mount', async () => { + const { result } = renderHook(() => useFetchProjectList('', 0, 9, false)); + + await waitFor(() => { + expect(result.current.projects).toEqual([ + { + projectId: '1', + name: 'Project 1', + tags: ['Tag1', 'Tag2'], + creationDate: '2023-10-01', + createdBy: 'User A', + }, + ]); + expect(result.current.count).toBe(1); + }); + }); + + it('fetches projects with search term', async () => { + renderHook(() => useFetchProjectList('test', 0, 9, false)); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=1&size=9&search=test'); + }); + }); + + it('fetches projects with pagination', async () => { + renderHook(() => useFetchProjectList('', 1, 9, false)); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=2&size=9&search='); + }); + }); +}); diff --git a/src/hooks/test/useHandlePinnedProjectList.test.tsx b/src/hooks/test/useHandlePinnedProjectList.test.tsx new file mode 100644 index 0000000..c6bc948 --- /dev/null +++ b/src/hooks/test/useHandlePinnedProjectList.test.tsx @@ -0,0 +1,101 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { Queries, renderHook, RenderHookOptions, waitFor } from '@testing-library/react'; +import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList'; +import { afterEach, beforeEach, describe, expectTypeOf, it, Mock, vi } from 'vitest'; +import { + PinnedProjectProvider, + PinnedProjectProviderProps, + usePinnedProjectDispatch, +} from '@/store/contexts/ProjectContext'; + +const mockProjectsApiResponse = [ + { + id: '1', + name: 'Bilan previsionnel 2027', + createdBy: 'MOUAD Paris test', + creationDate: '2024-07-25T10:09:41', + studies: [1, 2, 3], + tags: ['gaz', 'elec', 'antares', 'misc', 'tag2 antares', 'area link'], + description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + projectId: '1', + pinned: true, + }, + { + id: '2', + name: 'Bilan previsionnel 2023', + createdBy: 'Taher benjelloun amine', + creationDate: '2024-07-25T10:09:41', + studies: [6, 5, 9], + tags: ['bilan 22'], + description: 'description2023', + projectId: '2', + pinned: true, + }, + { + id: '3', + name: 'Bilan previsionnel 2025', + createdBy: 'zayd guillaume pegase', + creationDate: '2024-07-25T10:09:41', + studies: [7, 8], + tags: ['figma', 'config', 'modal'], + description: 'In the world of software development, achieving perfection is a journey rather than a destination.', + projectId: '3', + pinned: true, + }, +]; + +vi.mock('@/envVariables', () => ({ + getEnvVariables: vi.fn(() => 'https://mockapi.com'), +})); + +vi.mock('@/store/contexts/ProjectContext', async (importOriginal) => { + const actual: Mock = await importOriginal(); + const mockDispatch = vi.fn(); + return { + ...actual, + usePinnedProjectDispatch: vi.fn(() => mockDispatch), + }; +}); + +describe('useHandlePinnedProjectList', () => { + const mockUsePinnedProjectDispatch = usePinnedProjectDispatch as Mock; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should trigger getPinnedProject method on init and call dispatch to update pinned project list correctly', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => mockProjectsApiResponse, + }); + + const wrapper = ({ children, initialValue }: PinnedProjectProviderProps) => ( + + ); + + const { result } = renderHook(() => useHandlePinnedProjectList(), { + wrapper, + initialProps: { initialValue: { pinnedProject: [] } }, + } as RenderHookOptions<{ initialValue: { pinnedProject: never[] } }, Queries>); + + await waitFor(() => { + expectTypeOf(result.current.getPinnedProjects).toBeFunction(); + expectTypeOf(result.current.handleUnpinProject).toBeFunction(); + expectTypeOf(result.current.handlePinProject).toBeFunction(); + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/pinned?userId=me00247'); + expect(mockUsePinnedProjectDispatch).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/hooks/test/useNewStudyModal.test.ts b/src/hooks/test/useNewStudyModal.test.ts new file mode 100644 index 0000000..5429631 --- /dev/null +++ b/src/hooks/test/useNewStudyModal.test.ts @@ -0,0 +1,24 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useNewStudyModal } from '@/hooks/useNewStudyModal.ts'; +import { act, renderHook } from '@testing-library/react'; +import { expectTypeOf } from 'vitest'; + +describe('useNewStudyModal', () => { + it('should return isModalOpen and toggleModal function', () => { + const { result } = renderHook(() => useNewStudyModal()); + + expect(result.current.isModalOpen).toBe(false); + expectTypeOf(result.current.toggleModal).toBeFunction(); + + act(() => { + result.current.toggleModal(); + }); + + expect(result.current.isModalOpen).toBe(true); + }); +}); diff --git a/src/hooks/test/useProjectNavigation.test.tsx b/src/hooks/test/useProjectNavigation.test.tsx new file mode 100644 index 0000000..7ae3cec --- /dev/null +++ b/src/hooks/test/useProjectNavigation.test.tsx @@ -0,0 +1,63 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { afterEach, beforeEach, describe, expectTypeOf, it, Mock, vi } from 'vitest'; +import { act, Queries, renderHook, RenderHookOptions } from '@testing-library/react'; +import { Router, useNavigate } from 'react-router-dom'; +import { useProjectNavigation } from '@/hooks/useProjectNavigation'; +import { ReactNode } from 'react'; + +const mockNavigator = { + createHref: vi.fn(), + go: vi.fn(), + push: vi.fn(), + replace: vi.fn(), +}; + +vi.mock('react-router-dom', async (importOriginal) => { + const actual: Mock = await importOriginal(); + return { + ...actual, + useNavigate: vi.fn(), + }; +}); + +describe('useProjectNavigation', () => { + const mockUseNavigation = useNavigate as Mock; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return navigateToProject function and call navigate with the right parameters value', () => { + const mockNavigate = vi.fn().mockImplementation((to) => to); + mockUseNavigation.mockImplementationOnce(() => mockNavigate); + + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + const { result } = renderHook(() => useProjectNavigation(), { + wrapper, + } as RenderHookOptions); + + expectTypeOf(result.current.navigateToProject).toBeFunction(); + + act(() => { + result.current.navigateToProject('project123', 'projectName'); + }); + + expect(mockNavigate).toHaveBeenCalledWith(`/project/${encodeURIComponent('projectName')}`, { + state: { projectId: 'project123' }, + }); + }); +}); diff --git a/src/hooks/test/useStudyNavigation.test.tsx b/src/hooks/test/useStudyNavigation.test.tsx new file mode 100644 index 0000000..740c62e --- /dev/null +++ b/src/hooks/test/useStudyNavigation.test.tsx @@ -0,0 +1,75 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { afterEach, beforeEach, describe, expectTypeOf, it, Mock, vi } from 'vitest'; +import { act, Queries, renderHook, RenderHookOptions } from '@testing-library/react'; +import { useStudyNavigation } from '@/hooks/useStudyNavigation.ts'; +import { Router, useNavigate } from 'react-router-dom'; +import { ReactNode } from 'react'; + +const mockNavigator = { + createHref: vi.fn(), + go: vi.fn(), + push: vi.fn(), + replace: vi.fn(), +}; + +const mockStudy = { + id: 1, + name: 'BP_ref_1', + createdBy: 'Isaac Asimov', + creationDate: new Date('Janvier 18'), + keywords: ['covid', 'silence'], + project: 'Bilan previsionnel 2027', + status: 'missing', + horizon: '2020_2024', + trajectoryIds: [2], +}; + +vi.mock('react-router-dom', async (importOriginal) => { + const actual: Mock = await importOriginal(); + return { + ...actual, + useNavigate: vi.fn(), + }; +}); + +describe('useStudyNavigation', () => { + const mockUseNavigation = useNavigate as Mock; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.resetAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return navigateToStudy function and call navigate with the right parameters value', () => { + const mockNavigate = vi.fn().mockImplementation(vi.fn()); + mockUseNavigation.mockImplementationOnce(() => mockNavigate); + + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + const { result } = renderHook(() => useStudyNavigation(), { + wrapper, + } as RenderHookOptions); + + expectTypeOf(result.current.navigateToStudy).toBeFunction(); + + act(() => { + result.current.navigateToStudy(mockStudy); + }); + + expect(mockNavigate).toHaveBeenCalledWith(`/study/${encodeURIComponent(mockStudy.name)}`, { + state: { study: mockStudy }, + }); + }); +}); diff --git a/src/pages/pegase/home/components/tests/useStudyTableDisplay.test.ts b/src/hooks/test/useStudyTableDisplay.test.ts similarity index 60% rename from src/pages/pegase/home/components/tests/useStudyTableDisplay.test.ts rename to src/hooks/test/useStudyTableDisplay.test.ts index 63862c9..09f05a3 100644 --- a/src/pages/pegase/home/components/tests/useStudyTableDisplay.test.ts +++ b/src/hooks/test/useStudyTableDisplay.test.ts @@ -4,14 +4,23 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { renderHook, waitFor } from '@testing-library/react'; -import { useStudyTableDisplay } from '../useStudyTableDisplay'; +import { act, renderHook, waitFor } from '@testing-library/react'; +import { useStudyTableDisplay } from '@/hooks/useStudyTableDisplay'; +import { vi } from 'vitest'; + +vi.mock('@/envVariables', () => ({ + getEnvVariables: vi.fn(() => 'https://mockapi.com'), +})); describe('useStudyTableDisplay', () => { beforeEach(() => { global.fetch = vi.fn(); }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('fetches data and updates state correctly', async () => { const mockResponse = { content: [ @@ -43,19 +52,33 @@ describe('useStudyTableDisplay', () => { }); const { result } = renderHook(() => - useStudyTableDisplay({ searchStudy: 'test', sortBy: { status: 'desc' }, reloadStudies: true }), + useStudyTableDisplay({ searchTerm: 'test', sortBy: { status: 'desc' }, reloadStudies: true }), ); await waitFor(() => { expect(result.current.rows).toHaveLength(2); expect(result.current.rows).toEqual(mockResponse.content); + expect(result.current.count).toEqual(2); + //expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + 'https://mockapi.com/v1/study/search?page=1&size=9&projectId=&search=test&sortColumn=status&sortDirection=desc', + ); + }); + + await act(async () => { + renderHook(() => useStudyTableDisplay({ searchTerm: 'mouad', sortBy: { project: 'asc' }, reloadStudies: true })); }); + + //expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + 'https://mockapi.com/v1/study/search?page=1&size=9&projectId=&search=mouad&sortColumn=project&sortDirection=asc', + ); }); it('handles fetch error correctly', async () => { global.fetch = vi.fn().mockRejectedValue(new Error('Fetch error')); const { result } = renderHook(() => - useStudyTableDisplay({ searchStudy: 'test', sortBy: { status: 'desc' }, reloadStudies: true }), + useStudyTableDisplay({ searchTerm: 'test', sortBy: { status: 'desc' }, reloadStudies: true }), ); await waitFor(() => { @@ -87,12 +110,22 @@ describe('useStudyTableDisplay', () => { }); const { result } = renderHook(() => - useStudyTableDisplay({ searchStudy: 'study1', sortBy: { status: 'desc' }, reloadStudies: true }), + useStudyTableDisplay({ + searchTerm: 'study1', + projectId: 'projectId', + sortBy: { status: 'desc' }, + reloadStudies: true, + }), ); + act(() => { + result.current.setPage(3); + }); + await waitFor(() => { expect(result.current.rows).toHaveLength(1); - expect(result.current.current).toEqual(0); + expect(result.current.count).toEqual(1); + expect(result.current.currentPage).toEqual(3); }); }); }); diff --git a/src/components/pegase/pegaseCard/useDropdownOptions.ts b/src/hooks/useDropdownOptions.ts similarity index 50% rename from src/components/pegase/pegaseCard/useDropdownOptions.ts rename to src/hooks/useDropdownOptions.ts index 8400425..4d9a04c 100644 --- a/src/components/pegase/pegaseCard/useDropdownOptions.ts +++ b/src/hooks/useDropdownOptions.ts @@ -15,40 +15,46 @@ export const useDropdownOptions = () => { const { t } = useTranslation(); const settingOption = useCallback( - (onClick: () => void, label?: string, disabled?: boolean): RdsDropdownOption => ({ - key: 'setting', - label: label || t('project.@setting'), - value: 'setting', - icon: RdsIconId.Settings, - onItemClick: onClick, - extraClasses: NO_WRAP_CLASS, - disabled: disabled, - }), + (onClick: () => void, label?: string, disabled?: boolean): RdsDropdownOption => { + return { + key: 'setting', + label: label ?? t('project.@setting'), + value: 'setting', + onItemClick: onClick, + disabled: disabled, + icon: RdsIconId.Settings, + extraClasses: NO_WRAP_CLASS, + } as RdsDropdownOption; + }, [t], ); const deleteOption = useCallback( - (onClick: () => void, label?: string, disabled?: boolean): RdsDropdownOption => ({ - key: 'delete', - label: label ?? t('project.@delete'), - value: 'delete', - icon: RdsIconId.Delete, - onItemClick: onClick, - extraClasses: clsx(NO_WRAP_CLASS, '[&]:text-error-600 [&]:hover:text-error-600'), - disabled: disabled, - }), + (onClick: () => void, label?: string, disabled?: boolean): RdsDropdownOption => { + return { + key: 'delete', + label: label ?? t('project.@delete'), + value: 'delete', + icon: RdsIconId.Delete, + onItemClick: onClick, + extraClasses: clsx(NO_WRAP_CLASS, '[&]:text-error-600 [&]:hover:text-error-600'), + disabled: disabled, + } as RdsDropdownOption; + }, [t], ); const pinOption = useCallback( - (pinned: boolean, onClick: () => void): RdsDropdownOption => ({ - key: 'pin', - label: pinned ? t('project.@unpin') : t('project.@pin'), - value: 'pin', - icon: pinned ? RdsIconId.KeepOff : RdsIconId.PushPin, - onItemClick: onClick, - extraClasses: NO_WRAP_CLASS, - }), + (pinned: boolean, onClick: () => void): RdsDropdownOption => { + return { + key: 'pin', + label: pinned ? t('project.@unpin') : t('project.@pin'), + value: 'pin', + icon: pinned ? RdsIconId.KeepOff : RdsIconId.PushPin, + onItemClick: onClick, + extraClasses: NO_WRAP_CLASS, + } as RdsDropdownOption; + }, [t], ); diff --git a/src/hooks/useFetchProjectList.ts b/src/hooks/useFetchProjectList.ts new file mode 100644 index 0000000..8c52259 --- /dev/null +++ b/src/hooks/useFetchProjectList.ts @@ -0,0 +1,37 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useEffect, useState } from 'react'; +import { ProjectInfo } from '@/shared/types/pegase/Project.type.ts'; +import { getEnvVariables } from '@/envVariables.ts'; + +export const useFetchProjectList = ( + searchTerm: string, + current: number, + intervalSize: number, + shouldRefetch: boolean, +) => { + const [projects, setProjects] = useState([]); + const [count, setCount] = useState(0); + const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); + + const fetchProjects = useCallback(async () => { + const url = `${BASE_URL}/v1/project/search?page=${current + 1}&size=${intervalSize}&search=${searchTerm || ''}`; + fetch(url) + .then((response) => response.json()) + .then((json) => { + setProjects(json.content); + setCount(json.totalElements); + }) + .catch((error) => console.error(error)); + }, [current, intervalSize, searchTerm]); + + useEffect(() => { + void fetchProjects(); + }, [BASE_URL, current, searchTerm, intervalSize, shouldRefetch]); + + return { projects, count, refetch: fetchProjects }; +}; diff --git a/src/hooks/useFocusTrapping.ts b/src/hooks/useFocusTrapping.ts deleted file mode 100644 index 4f9df88..0000000 --- a/src/hooks/useFocusTrapping.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useEffect } from 'react'; - -const FOCUSABLE_ELEMENTS = [ - 'button', - 'a[href]', - 'input', - 'select', - 'textarea', - 'details', - '[tabindex]:not([tabindex="-1"])', -]; -const FOCUSABLE_ELEMENTS_QUERY = FOCUSABLE_ELEMENTS.map((elmt) => elmt + ':not([disabled]):not([aria-hidden])').join( - ',', -); - -const getFocusableElements = (containerElement: HTMLElement) => { - const elmts = containerElement.querySelectorAll(FOCUSABLE_ELEMENTS_QUERY) as unknown as HTMLElement[]; - return [elmts[0], elmts[elmts.length - 1]]; -}; - -const useFocusTrapping = (ref: React.RefObject, show: boolean) => { - useEffect(() => { - if (!show || !ref.current) { - return; - } - const containerElement = ref.current; - - const handleTabKeyPress = (event: KeyboardEvent) => { - const [firstElement, lastElement] = getFocusableElements(containerElement); - if (event.key === 'Tab') { - if (event.shiftKey && document.activeElement === firstElement) { - event.preventDefault(); - lastElement.focus(); - } else if (!event.shiftKey && document.activeElement === lastElement) { - event.preventDefault(); - firstElement.focus(); - } - } - }; - - containerElement.addEventListener('keydown', handleTabKeyPress); - - return () => { - containerElement.removeEventListener('keydown', handleTabKeyPress); - }; - }, [ref, show]); -}; - -export default useFocusTrapping; diff --git a/src/hooks/useHandlePinnedProjectList.ts b/src/hooks/useHandlePinnedProjectList.ts new file mode 100644 index 0000000..70ff9a9 --- /dev/null +++ b/src/hooks/useHandlePinnedProjectList.ts @@ -0,0 +1,122 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useEffect } from 'react'; +import { PinnedProjectActionType } from '@/shared/types/pegase/Project.type'; +import { fetchPinnedProjects, pinProject, unpinProject } from '@/shared/services/pinnedProjectService'; +import { PINNED_PROJECT_ACTION } from '@/shared/enum/project'; +import { usePinnedProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; +import { v4 as uuidv4 } from 'uuid'; +import { dismissToast, notifyToast, NotifyWithActionProps } from '@/shared/notification/notification.tsx'; +import { useTranslation } from 'react-i18next'; + +export const useHandlePinnedProjectList = () => { + const userId = 'me00247'; + const dispatch = usePinnedProjectDispatch(); + const { t } = useTranslation(); + + const getPinnedProjects = useCallback(async () => { + try { + const projects = await fetchPinnedProjects(userId); + if (projects?.length) { + dispatch?.({ + type: PINNED_PROJECT_ACTION.INIT_LIST, + payload: projects, + } as PinnedProjectActionType); + } + } catch (error) { + // silent handler + } + }, []); + + useEffect(() => { + void getPinnedProjects(); + }, []); + + /** + * Handles the pin action. Displays a toast if the API call is successful. + * + * @param {string} projectId - Project id + */ + const handlePinProject = useCallback(async (projectId: string) => { + const toastId = uuidv4(); + try { + const newProject = await pinProject(projectId); + + if (newProject) { + dispatch?.({ + type: PINNED_PROJECT_ACTION.ADD_ITEM, + payload: newProject, + } as PinnedProjectActionType); + } + + notifyToast({ + id: toastId, + type: 'success', + message: 'Project pinned successfully', + }); + } catch (error: unknown) { + notifyToast({ + id: toastId, + type: 'error', + message: 'Project already pinned', + }); + } + }, []); + + /** + * Handles the unpin action. Displays a toast if the API call is successful. + * The API call to the /unpin endpoint is made only if the "Cancel" button + * on the toast is not clicked. + * + * @param {string} projectId - Project id + */ + const handleUnpinProject = useCallback(async (projectId: string) => { + let apiCallTimeout: number | null = null; + const toastId = uuidv4(); + const userId = 'me00247'; + const currentPinnedProjects = await fetchPinnedProjects(userId); + + dispatch?.({ + type: PINNED_PROJECT_ACTION.REMOVE_ITEM, + payload: projectId, + } as PinnedProjectActionType); + + notifyToast({ + id: toastId, + type: 'info', + message: t('components.quickAccess.@confirmUnpin', { name: projectId }), + action: { + label: t('components.quickAccess.@cancel'), + onClick: () => { + dismissToast(toastId); + clearTimeout(apiCallTimeout!); + dispatch?.({ + type: PINNED_PROJECT_ACTION.INIT_LIST, + payload: currentPinnedProjects, + } as PinnedProjectActionType); + }, + }, + } as NotifyWithActionProps); + + apiCallTimeout = setTimeout(() => { + unpinProject(userId, projectId).catch((error) => { + dispatch?.({ + type: PINNED_PROJECT_ACTION.INIT_LIST, + payload: currentPinnedProjects, + } as PinnedProjectActionType); + + notifyToast({ + id: toastId, + type: 'error', + message: `${error.message}`, + }); + }); + }, 4000) as unknown as number; + }, []); + + return { getPinnedProjects, handlePinProject, handleUnpinProject }; +}; diff --git a/src/hooks/useInputFormState.ts b/src/hooks/useInputFormState.ts deleted file mode 100644 index d43d9ee..0000000 --- a/src/hooks/useInputFormState.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { StdChangeHandler } from '@/shared/types'; -import { useEffect, useRef, useState } from 'react'; -import useDebounce from './common/useDebounce'; - -export const useInputFormState = ( - value: TValue, - defaultValue: TValue, - onChange: StdChangeHandler | undefined, - updateRef?: (value: TValue) => void, -) => { - const [stateValue, setStateValue] = useState(defaultValue || value); - const updateRefRef = useRef(updateRef); - - const renderCount = useRef(0); - renderCount.current += 1; - useEffect(() => { - if (renderCount.current > 1) { - setStateValue(value); - updateRefRef.current?.(value); - } - }, [value]); - - useEffect(() => { - updateRefRef.current?.(stateValue); - }, [stateValue]); - - useDebounce(async () => await onChange?.(stateValue ?? defaultValue), stateValue); - - return { value: stateValue, setValue: setStateValue }; -}; diff --git a/src/hooks/useModal.ts b/src/hooks/useModal.ts deleted file mode 100644 index 99d5281..0000000 --- a/src/hooks/useModal.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { ModalContext } from '@/contexts/modalContext'; -import { useContext } from 'react'; - -const useModal = () => useContext(ModalContext); - -export default useModal; diff --git a/src/hooks/useNewStudyModal.ts b/src/hooks/useNewStudyModal.ts new file mode 100644 index 0000000..c2acec0 --- /dev/null +++ b/src/hooks/useNewStudyModal.ts @@ -0,0 +1,20 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useState } from 'react'; + +export function useNewStudyModal() { + const [isModalOpen, setIsModalOpen] = useState(false); + + const toggleModal = () => { + setIsModalOpen((prev) => !prev); + }; + + return { + isModalOpen, + toggleModal, + }; +} diff --git a/src/hooks/useProjectNavigation.ts b/src/hooks/useProjectNavigation.ts index 2c72525..658944e 100644 --- a/src/hooks/useProjectNavigation.ts +++ b/src/hooks/useProjectNavigation.ts @@ -5,15 +5,16 @@ */ import { useNavigate } from 'react-router-dom'; +import { useCallback } from 'react'; export const useProjectNavigation = () => { const navigate = useNavigate(); - const navigateToProject = (projectId: string, projectName: string) => { + const navigateToProject = useCallback((projectId: string, projectName: string) => { navigate(`/project/${encodeURIComponent(projectName)}`, { state: { projectId }, }); - }; + }, []); return { navigateToProject }; }; diff --git a/src/hooks/useStdId.ts b/src/hooks/useStdId.ts deleted file mode 100644 index 3b2d1eb..0000000 --- a/src/hooks/useStdId.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useId } from 'react'; - -export const useStdId = (prefix: string, id?: string): string => { - const reactId = useId(); - return id || `${prefix}-${reactId}`; -}; diff --git a/src/pages/pegase/studies/useStudyNavigation.ts b/src/hooks/useStudyNavigation.ts similarity index 75% rename from src/pages/pegase/studies/useStudyNavigation.ts rename to src/hooks/useStudyNavigation.ts index a074fc4..8c2f568 100644 --- a/src/pages/pegase/studies/useStudyNavigation.ts +++ b/src/hooks/useStudyNavigation.ts @@ -5,16 +5,17 @@ */ import { useNavigate } from 'react-router-dom'; -import {StudyDTO} from "@/shared/types"; +import { StudyDTO } from '@/shared/types'; +import { useCallback } from 'react'; export const useStudyNavigation = () => { const navigate = useNavigate(); - const navigateToStudy = (study: StudyDTO) => { + const navigateToStudy = useCallback((study: StudyDTO) => { navigate(`/study/${encodeURIComponent(study.name)}`, { state: { study }, }); - }; + }, []); return { navigateToStudy }; }; diff --git a/src/hooks/useStudyTableDisplay.ts b/src/hooks/useStudyTableDisplay.ts new file mode 100644 index 0000000..6bb827e --- /dev/null +++ b/src/hooks/useStudyTableDisplay.ts @@ -0,0 +1,68 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'; +import { StudyDTO } from '@/shared/types'; +import { fetchSearchStudies } from '@/shared/services/studyService.ts'; + +const ITEMS_PER_PAGE = 9; +const PAGINATION_CURRENT = 0; +const intervalSize = ITEMS_PER_PAGE; + +interface UseStudyTableDisplayProps { + searchTerm: string | undefined; + projectId?: string; + sortBy: { [key: string]: 'asc' | 'desc' }; + reloadStudies: boolean; +} + +interface UseStudyTableDisplayReturn { + rows: StudyDTO[]; + count: number; + intervalSize: number; + currentPage: number; + setPage: Dispatch>; + error: unknown; +} + +export const useStudyTableDisplay = ({ + searchTerm, + projectId, + sortBy, + reloadStudies, +}: UseStudyTableDisplayProps): UseStudyTableDisplayReturn => { + const [rows, setRows] = useState([]); + const [count, setCount] = useState(0); + const [currentPage, setCurrentPage] = useState(0); + const [error, setError] = useState(null); + + const searchTermRef = useRef(searchTerm); + const projectIdRef = useRef(projectId); + const sortByRef = useRef(sortBy); + const reloadStudiesRef = useRef(reloadStudies); + + useEffect(() => { + setCurrentPage(PAGINATION_CURRENT); + }, []); + + useEffect(() => { + searchTermRef.current = searchTerm; + projectIdRef.current = projectId; + sortByRef.current = sortBy; + reloadStudiesRef.current = reloadStudies; + }, [searchTerm, projectId, sortBy, reloadStudies]); + + useEffect(() => { + fetchSearchStudies(searchTermRef.current, projectIdRef.current, currentPage, intervalSize, sortByRef.current) + .then(({ content, totalElements }) => { + setRows(content); + setCount(totalElements); + }) + .catch((error) => setError(error)); + }, [currentPage, searchTerm, projectId, sortBy, reloadStudies]); + + return { rows, count, intervalSize, currentPage, setPage: setCurrentPage, error }; +}; diff --git a/src/pages/pegase/home/HomePage.tsx b/src/pages/pegase/home/HomePage.tsx index b1aa233..b4fdd1e 100644 --- a/src/pages/pegase/home/HomePage.tsx +++ b/src/pages/pegase/home/HomePage.tsx @@ -6,18 +6,16 @@ import HomePageContent from './components/HomePageContent'; import PinnedProject from '@/pages/pegase/home/pinnedProjects/PinnedProject'; -import { useState } from 'react'; +import { PinnedProjectProvider } from '@/store/contexts/ProjectContext.tsx'; const HomePage = () => { - const [reloadPinnedProject, isReloadPinnedProject] = useState(true); - return ( - <> +
- +
- +
); }; diff --git a/src/pages/pegase/home/components/StudyTableDisplay.tsx b/src/pages/pegase/home/components/StudyTableDisplay.tsx index ac2e1a6..acf37b8 100644 --- a/src/pages/pegase/home/components/StudyTableDisplay.tsx +++ b/src/pages/pegase/home/components/StudyTableDisplay.tsx @@ -5,19 +5,20 @@ */ import { useState } from 'react'; -import StdSimpleTable from '@/components/common/data/stdSimpleTable/StdSimpleTable'; -import { StudyDTO } from '@/shared/types/index'; +import { useTranslation } from 'react-i18next'; +import { StudyDTO } from '@/shared/types'; +import { StudyStatus } from '@/shared/types/common/StudyStatus.type'; import getStudyTableHeaders from './StudyTableHeaders'; -import { addSortColumn, useNewStudyModal } from './StudyTableUtils'; +import { addSortColumn } from './StudyTableUtils'; import StudiesPagination from './StudiesPagination'; import { RowSelectionState } from '@tanstack/react-table'; -import { StudyStatus } from '@/shared/types/common/StudyStatus.type'; -import { useStudyTableDisplay } from './useStudyTableDisplay'; -import { useTranslation } from 'react-i18next'; import StudyCreationModal from '../../studies/StudyCreationModal'; -import { handleDelete } from '@/pages/pegase/home/components/studyService'; -import {RdsButton} from "rte-design-system-react"; -import { useStudyNavigation } from '@/pages/pegase/studies/useStudyNavigation'; +import { deleteStudy } from '@/pages/pegase/home/components/studyService'; +import StdSimpleTable from '@/components/common/data/stdSimpleTable/StdSimpleTable'; +import { RdsButton } from 'rte-design-system-react'; +import { useStudyTableDisplay } from '@/hooks/useStudyTableDisplay'; +import { useNewStudyModal } from '@/hooks/useNewStudyModal'; +import { useStudyNavigation } from '@/hooks/useStudyNavigation'; interface StudyTableDisplayProps { searchStudy: string | undefined; @@ -25,23 +26,23 @@ interface StudyTableDisplayProps { } const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) => { - const [sortByState, setSortByState] = useState<{ [key: string]: 'asc' | 'desc' }>({}); + const { t } = useTranslation(); const [rowSelection, setRowSelection] = useState({}); - const [sortedColumn, setSortedColumn] = useState('status'); const [isHeaderHovered, setIsHeaderHovered] = useState(false); - const { isModalOpen, toggleModal } = useNewStudyModal(); - const { t } = useTranslation(); const [selectedStudy, setSelectedStudy] = useState(null); - // Reload trigger for re-fetching data const [reloadStudies, setReloadStudies] = useState(false); - const { navigateToStudy } = useStudyNavigation(); + const [sortBy, setSortBy] = useState<{ [key: string]: 'asc' | 'desc' }>({}); + const [sortedColumn, setSortedColumn] = useState('status'); - const handleSort = (column: string) => { - const newSortOrder = sortByState[column] === 'asc' ? 'desc' : 'asc'; - setSortByState({ [column]: newSortOrder }); - setSortedColumn(column); - }; + const { isModalOpen, toggleModal } = useNewStudyModal(); + const { navigateToStudy } = useStudyNavigation(); + const { rows, count, intervalSize, currentPage, setPage } = useStudyTableDisplay({ + searchTerm: searchStudy, + projectId, + sortBy, + reloadStudies, // Key change here + }); const handleHeaderHover = (hovered: boolean) => { setIsHeaderHovered(hovered); @@ -49,21 +50,11 @@ const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) = const headers = getStudyTableHeaders(); - const sortedHeaders = addSortColumn( - headers, - handleSort, - sortByState, - sortedColumn, - handleHeaderHover, - isHeaderHovered, - ); - - const { rows, count, intervalSize, current, setPage } = useStudyTableDisplay({ - searchStudy, - projectId, - sortBy: sortByState, - reloadStudies, // Key change here - }); + const handleSort = (column: string) => { + const newSortOrder = sortBy[column] === 'asc' ? 'desc' : 'asc'; + setSortBy({ [column]: newSortOrder }); + setSortedColumn(column); + }; const selectedRowId = Object.keys(rowSelection)[0]; const selectedStatus = rows[Number.parseInt(selectedRowId || '-1')]?.status?.toUpperCase(); @@ -80,11 +71,12 @@ const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) = const handleDeleteClick = () => { const selectedStudyId = rows[Number.parseInt(selectedRowId || '-1')]?.id; if (selectedStudyId) { - handleDelete(selectedStudyId).then(() => { + deleteStudy(selectedStudyId).then(() => { setReloadStudies(!reloadStudies); // Trigger reload after deleting }); } }; + const handleStudyClick = (study: StudyDTO) => { navigateToStudy(study); }; @@ -93,6 +85,9 @@ const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) = const selectedStudy = rows[Number.parseInt(selectedRowId || '-1')]; handleStudyClick(selectedStudy); }; + + const sortedHeaders = addSortColumn(headers, handleSort, sortBy, sortedColumn, handleHeaderHover, isHeaderHovered); + return (
@@ -140,7 +135,7 @@ const StudyTableDisplay = ({ searchStudy, projectId }: StudyTableDisplayProps) = /> )}
- +
); diff --git a/src/pages/pegase/home/components/StudyTableHeaders.tsx b/src/pages/pegase/home/components/StudyTableHeaders.tsx index 59e2c3a..b67f107 100644 --- a/src/pages/pegase/home/components/StudyTableHeaders.tsx +++ b/src/pages/pegase/home/components/StudyTableHeaders.tsx @@ -6,7 +6,7 @@ import StdAvatar from '@/components/common/layout/stdAvatar/StdAvatar'; import { StudyStatus } from '@/shared/types/common/StudyStatus.type'; -import { StudyDTO } from '@/shared/types/pegase/study'; +import { StudyDTO } from '@/shared/types/pegase/Study.type.ts'; import { createColumnHelper } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; import { RdsRadioButton, RdsTagList } from 'rte-design-system-react'; diff --git a/src/pages/pegase/home/components/StudyTableUtils.tsx b/src/pages/pegase/home/components/StudyTableUtils.tsx index 1438ae2..f28e4af 100644 --- a/src/pages/pegase/home/components/StudyTableUtils.tsx +++ b/src/pages/pegase/home/components/StudyTableUtils.tsx @@ -6,7 +6,6 @@ import { StdIconId } from '@/shared/utils/common/mappings/iconMaps'; import StdIcon from '@common/base/stdIcon/StdIcon'; -import { useState } from 'react'; export function addSortColumn( headers: any[], @@ -61,16 +60,3 @@ export function addSortColumn( }; }); } - -export function useNewStudyModal() { - const [isModalOpen, setModalOpen] = useState(false); - - const toggleModal = () => { - setModalOpen((prev) => !prev); - }; - - return { - isModalOpen, - toggleModal, - }; -} diff --git a/src/pages/pegase/home/components/studyService.ts b/src/pages/pegase/home/components/studyService.ts index 3ef2773..706bfc6 100644 --- a/src/pages/pegase/home/components/studyService.ts +++ b/src/pages/pegase/home/components/studyService.ts @@ -52,7 +52,7 @@ export const fetchSuggestedKeywords = async (query: string): Promise = return data; }; -export const handleDelete = async (id: number) => { +export const deleteStudy = async (id: number) => { try { const response = await fetch(`${BASE_URL}/v1/study/${id}`, { method: 'DELETE', diff --git a/src/pages/pegase/home/components/useStudyTableDisplay.ts b/src/pages/pegase/home/components/useStudyTableDisplay.ts deleted file mode 100644 index 98a48d7..0000000 --- a/src/pages/pegase/home/components/useStudyTableDisplay.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useEffect, useState } from 'react'; -import { StudyDTO } from '@/shared/types/index'; -import { getEnvVariables } from '@/envVariables'; - -const ITEMS_PER_PAGE = 10; -const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); -const PAGINATION_CURRENT = 0; -const PAGINATION_COUNT = 0; - -interface UseStudyTableDisplayProps { - searchStudy: string | undefined; - projectId?: string; - sortBy: { [key: string]: 'asc' | 'desc' }; - reloadStudies: boolean; -} - -interface UseStudyTableDisplayReturn { - rows: StudyDTO[]; - count: number; - intervalSize: number; - current: number; - setPage: React.Dispatch>; -} - -export const useStudyTableDisplay = ({ - searchStudy, - projectId, - sortBy, - reloadStudies, -}: UseStudyTableDisplayProps): UseStudyTableDisplayReturn => { - const [rows, setRows] = useState([]); - const [count, setCount] = useState(0); - const [current, setCurrent] = useState(0); - - const intervalSize = ITEMS_PER_PAGE; - - useEffect(() => { - setCurrent(PAGINATION_CURRENT); - setCount(PAGINATION_COUNT); - }, [searchStudy, projectId, sortBy, reloadStudies]); - - useEffect(() => { - const [sortColumn, sortDirection] = Object.entries(sortBy)[0] || ['', '']; - - fetch( - `${BASE_URL}/v1/study/search?page=${current + 1}&size=${intervalSize}&projectId=${projectId}&search=${searchStudy}&sortColumn=${sortColumn}&sortDirection=${sortDirection}`, - ) - .then((response) => response.json()) - .then((json) => { - setRows(json.content); - setCount(json.totalElements); - }) - .catch((error) => console.error(error)); - }, [current, searchStudy, projectId, sortBy, reloadStudies]); - - return { rows, count, intervalSize, current, setPage: setCurrent }; -}; diff --git a/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx b/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx index ccb4db9..581f933 100644 --- a/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx +++ b/src/pages/pegase/home/pinnedProjects/PinnedProject.tsx @@ -6,17 +6,17 @@ import PinnedProjectCards from '@/pages/pegase/home/pinnedProjects/PinnedProjectCard'; import ProjectCreator from '@/pages/pegase/home/pinnedProjects/ProjectCreator'; +import { Dispatch, SetStateAction } from 'react'; interface PinnedProjectProps { - reloadPinnedProject: boolean; - isReloadPinnedProject: (value: boolean) => void; + setShouldRefetchProjectList?: Dispatch>; } -const PinnedProject: React.FC = ({ reloadPinnedProject, isReloadPinnedProject }) => { +const PinnedProject = ({ setShouldRefetchProjectList }: PinnedProjectProps) => { return (
- +
); }; diff --git a/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx b/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx index f4403fa..eb6568c 100644 --- a/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx +++ b/src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx @@ -4,169 +4,108 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { ProjectInfo } from '@/shared/types/pegase/Project.type'; import PegaseCard from '@/components/pegase/pegaseCard/pegaseCard'; import StdAvatar from '@common/layout/stdAvatar/StdAvatar'; import { formatDateToDDMMYYYY } from '@/shared/utils/dateFormatter'; -import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { getEnvVariables } from '@/envVariables'; -import { useDropdownOptions } from '@/components/pegase/pegaseCard/useDropdownOptions'; -import { dismissToast, notifyToast } from '@/shared/notification/notification'; -import { v4 as uuidv4 } from 'uuid'; +import { useDropdownOptions } from '@/hooks/useDropdownOptions'; import { useProjectNavigation } from '@/hooks/useProjectNavigation'; import { RdsIcon, RdsIconId, RdsTagList } from 'rte-design-system-react'; -import { deleteProjectById } from '@/pages/pegase/projects/projectService'; - -export const PinnedProjectCards = ({ - reloadPinnedProject, - isReloadPinnedProject, -}: { - reloadPinnedProject: boolean; - isReloadPinnedProject: (value: boolean) => void; -}) => { - const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - const [projects, setProjects] = useState([]); +import { deleteProjectById } from '@/shared/services/projectService'; +import { usePinnedProject, usePinnedProjectDispatch } from '@/store/contexts/ProjectContext'; +import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList.ts'; +import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { PinnedProjectActionType } from '@/shared/types/pegase/Project.type.ts'; +import { notifyToast } from '@/shared/notification/notification.tsx'; +import { Dispatch, SetStateAction } from 'react'; + +interface PinnedProjectCardsProps { + setShouldRefetchProjectList?: Dispatch>; +} + +const PinnedProjectCards = ({ setShouldRefetchProjectList }: PinnedProjectCardsProps) => { const { t } = useTranslation(); const { navigateToProject } = useProjectNavigation(); - - const userId = 'me00247'; - - /** - * Handles the unpin action. Displays a toast if the API call is successful. - * The API call to the /unpin endpoint is made only if the "Cancel" button - * on the toast is not clicked. - */ - const handleUnpin = (projectId: string) => { - const oldProjects = [...projects]; - let apiCallTimeout: number | null = null; - const toastId = uuidv4(); - - setProjects((prevProjects) => prevProjects.filter((project) => project.id !== projectId)); - - const unpinApiCall = async () => { - try { - await fetch(`${BASE_URL}/v1/project/unpin?userId=${userId}&projectId=${projectId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - }); - } catch (error) { - console.error(`Error unpinning project ${projectId}:`, error); - } - }; - - notifyToast({ - id: toastId, - type: 'info', - message: t('components.quickAccess.@confirmUnpin', { name: projectId }), - action: { - label: t('components.quickAccess.@cancel'), - onClick: () => { - dismissToast(toastId); - clearTimeout(apiCallTimeout!); - setProjects(oldProjects); - }, - }, - }); - apiCallTimeout = setTimeout(() => { - unpinApiCall(); - }, 5000) as unknown as number; - }; - - const fetchPinnedProjects = async (baseUrl: string): Promise => { - try { - const response = await fetch(`${baseUrl}/v1/project/pinned?userId=${userId}`); - const json = await response.json(); - - return json.map((project: any) => ({ - ...project, - projectId: project.id.toString(), - //Projects in homepage should have pinned to TRUE - pinned: project.pinned ?? true, - })); - } catch (error) { - console.error('Failed to fetch pinned projects:', error); - throw error; - } - }; - - const loadPinnedProjects = async () => { - try { - const projects = await fetchPinnedProjects(BASE_URL); - setProjects(projects); - isReloadPinnedProject(false); - } catch (error) { - console.error('Error loading pinned projects:', error); - } - }; - - useEffect(() => { - if (reloadPinnedProject) { - loadPinnedProjects(); - } - }, [BASE_URL, reloadPinnedProject]); - const { settingOption, deleteOption, pinOption } = useDropdownOptions(); + const { pinnedProjects } = usePinnedProject(); + const dispatch = usePinnedProjectDispatch(); + const { handleUnpinProject } = useHandlePinnedProjectList(); const handleCardClick = (projectId: string, projectName: string) => { navigateToProject(projectId, projectName); }; + const deleteProject = async (projectId: string) => { - await deleteProjectById(projectId, isReloadPinnedProject); - await loadPinnedProjects(); + try { + await deleteProjectById(projectId); + setShouldRefetchProjectList?.(true); + dispatch?.({ + type: PINNED_PROJECT_ACTION.REMOVE_ITEM, + payload: projectId, + } as PinnedProjectActionType); + notifyToast({ + type: 'success', + message: 'Project deleted successfully', + }); + } catch (error: unknown) { + if (error instanceof Error) { + notifyToast({ + type: 'error', + message: `${error.message}`, + }); + } + } }; - return projects.map((project, index) => { - const dropdownItems = [ - pinOption(project.pinned ?? false, () => handleUnpin(project.id)), // Toggle pin/unpin - settingOption(() => {}, t('project.@setting')), - deleteOption(() => deleteProject(project.id), t('project.@delete'), project.studies?.length > 0), - ]; + return ( + <> + {pinnedProjects?.map((project, index) => ( +
+ handleUnpinProject(project.id)), // Toggle pin/unpin + settingOption(() => {}, t('project.@setting')), + deleteOption(() => deleteProject(project.id), t('project.@delete'), project.studies?.length > 0), + ]} + id={project.id} + onClick={() => handleCardClick(project.id, project.name)} + icons={ +
+ {' '} +
+ } + > +
+
+ {project.tags && ( +
+ +
+ )} +
- return ( -
- handleCardClick(project.id, project.name)} - icons={ -
- {' '} -
- } - > -
-
- {project.tags && ( -
- +
+
+ {t('project.@created')} :{' '} + {formatDateToDDMMYYYY(project.creationDate)} {' '} + {t('project.@by')} :
- )} -
-
-
- {t('project.@created')} :{' '} - {formatDateToDDMMYYYY(project.creationDate)} {' '} - {t('project.@by')} : + + {project.createdBy}
- - - {project.createdBy}
-
- -
- ); - }); + +
+ ))} + + ); }; export default PinnedProjectCards; diff --git a/src/pages/pegase/home/pinnedProjects/tests/handleUnpin.test.ts b/src/pages/pegase/home/pinnedProjects/tests/handleUnpin.test.ts deleted file mode 100644 index 7a1a23b..0000000 --- a/src/pages/pegase/home/pinnedProjects/tests/handleUnpin.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { ProjectInfo } from '@/shared/types/pegase/Project.type'; - -describe('handleUnpin test', () => { - it('should update the projects list by removing the unpinned project', () => { - let updatedProjects: ProjectInfo[] | null = null; - - const mockSetProjects = (callback: (prevProjects: ProjectInfo[]) => ProjectInfo[]) => { - const prevProjects: ProjectInfo[] = [ - { - id: '1', - name: 'Project 1', - description: 'project1', - createdBy: 'Luis Rodriguez', - creationDate: new Date(), - path: '', - tags: ['tag1', 'tag2'], - studies: [], - }, - { - id: '2', - name: 'Project 2', - description: 'project2', - createdBy: 'Maria Perez', - creationDate: new Date(), - path: '', - tags: ['tag3', 'tag4'], - studies: [], - }, - ]; - updatedProjects = callback(prevProjects); - }; - - const handleUnpin = ( - projectId: string, - setProjects: (callback: (prevProjects: ProjectInfo[]) => ProjectInfo[]) => void, - ) => { - setProjects((prevProjects) => prevProjects.filter((project) => project.id !== projectId)); - }; - - handleUnpin('1', mockSetProjects); - - expect(updatedProjects).toEqual([ - { - id: '2', - name: 'Project 2', - description: 'project2', - createdBy: 'Maria Perez', - creationDate: expect.any(Date), - path: '', - tags: ['tag3', 'tag4'], - studies: [], - }, - ]); - }); -}); diff --git a/src/pages/pegase/projects/ProjectContent.tsx b/src/pages/pegase/projects/ProjectContent.tsx index ec4cab1..fb95e9b 100644 --- a/src/pages/pegase/projects/ProjectContent.tsx +++ b/src/pages/pegase/projects/ProjectContent.tsx @@ -4,7 +4,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -// src/pages/pegase/projects/ProjectContent.tsx import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import SearchBar from '@/pages/pegase/home/components/SearchBar'; @@ -12,26 +11,36 @@ import PegaseCard from '@/components/pegase/pegaseCard/pegaseCard'; import { formatDateToDDMMYYYY } from '@/shared/utils/dateFormatter'; import StdAvatar from '@common/layout/stdAvatar/StdAvatar'; import StudiesPagination from '@/pages/pegase/home/components/StudiesPagination'; -import { useDropdownOptions } from '@/components/pegase/pegaseCard/useDropdownOptions'; - +import { useDropdownOptions } from '@/hooks/useDropdownOptions'; import { useProjectNavigation } from '@/hooks/useProjectNavigation'; -import { deleteProjectById, pinProject, useFetchProjects } from './projectService'; +import { deleteProjectById } from '@/shared/services/projectService.ts'; import { RdsChip, RdsTagList } from 'rte-design-system-react'; +import { useFetchProjectList } from '@/hooks/useFetchProjectList'; +import { useHandlePinnedProjectList } from '@/hooks/useHandlePinnedProjectList.ts'; +import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; +import { PinnedProjectActionType } from '@/shared/types/pegase/Project.type.ts'; +import { usePinnedProjectDispatch } from '@/store/contexts/ProjectContext.tsx'; interface ProjectContentProps { - isReloadPinnedProject: (value: boolean) => void; + shouldRefetchProjectList: boolean; } -const ProjectContent = ({ isReloadPinnedProject }: ProjectContentProps) => { +const ProjectContent = ({ shouldRefetchProjectList }: ProjectContentProps) => { const { t } = useTranslation(); const intervalSize = 9; + const userName = 'mouad'; // Replace with actual user name const [searchTerm, setSearchTerm] = useState(''); const [activeChip, setActiveChip] = useState(false); - const userName = 'mouad'; // Replace with actual user name const [current, setCurrent] = useState(0); - const { projects, count, refetch } = useFetchProjects(searchTerm || '', current, intervalSize); - + const { projects, count, refetch } = useFetchProjectList( + searchTerm || '', + current, + intervalSize, + shouldRefetchProjectList, + ); const { navigateToProject } = useProjectNavigation(); + const { handlePinProject } = useHandlePinnedProjectList(); + const dispatch = usePinnedProjectDispatch(); const searchProject = (value?: string | undefined) => { setSearchTerm(value); @@ -47,13 +56,13 @@ const ProjectContent = ({ isReloadPinnedProject }: ProjectContentProps) => { } }; - const handlePinProject = (projectId: string) => { - pinProject(projectId, isReloadPinnedProject); - }; - const deleteProject = async (projectId: string) => { - await deleteProjectById(projectId, isReloadPinnedProject); - refetch(); // Actualiser les projets après suppression + await deleteProjectById(projectId); + dispatch?.({ + type: PINNED_PROJECT_ACTION.REMOVE_ITEM, + payload: projectId, + } as PinnedProjectActionType); + await refetch(); // Actualiser les projets après suppression }; const handleCardClick = (projectId: string, projectName: string) => { @@ -75,7 +84,7 @@ const ProjectContent = ({ isReloadPinnedProject }: ProjectContentProps) => {
{projects.map((project) => { const dropdownItems = [ - pinOption(false, () => handlePinProject(project.id)), + pinOption(false, async () => handlePinProject(project.id)), settingOption(() => {}, t('project.@setting')), deleteOption(() => deleteProject(project.id), t('project.@delete'), project.studies?.length > 0), ]; diff --git a/src/pages/pegase/projects/Projects.tsx b/src/pages/pegase/projects/Projects.tsx deleted file mode 100644 index 7a1fc67..0000000 --- a/src/pages/pegase/projects/Projects.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { useState } from 'react'; -import PinnedProject from '@/pages/pegase/home/pinnedProjects/PinnedProject'; -import ProjectContent from '@/pages/pegase/projects/ProjectContent'; - -export const Projects = () => { - const [reloadPinnedProject, isReloadPinnedProject] = useState(true); - - return ( -
- - -
- ); -}; - -export default Projects; diff --git a/src/pages/pegase/projects/ProjectsPage.tsx b/src/pages/pegase/projects/ProjectsPage.tsx new file mode 100644 index 0000000..9b809ff --- /dev/null +++ b/src/pages/pegase/projects/ProjectsPage.tsx @@ -0,0 +1,24 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import PinnedProject from '@/pages/pegase/home/pinnedProjects/PinnedProject'; +import ProjectContent from '@/pages/pegase/projects/ProjectContent'; +import { PinnedProjectProvider } from '@/store/contexts/ProjectContext.tsx'; +import { useState } from 'react'; + +const ProjectsPage = () => { + const [shouldRefetchProjectList, setShouldRefetchProjectList] = useState(false); + return ( + +
+ + +
+
+ ); +}; + +export default ProjectsPage; diff --git a/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx b/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx index 56e6ad1..0385101 100644 --- a/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx +++ b/src/pages/pegase/projects/projectDetails/ProjectDetails.tsx @@ -4,16 +4,16 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useState, useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { ProjectInfo } from '@/shared/types/pegase/Project.type'; -import { getEnvVariables } from '@/envVariables'; import ProjectDetailsHeader from './ProjectDetailsHeader'; import ProjectDetailsContent from './ProjectDetailsContent'; import StudyTableDisplay from '@/pages/pegase/home/components/StudyTableDisplay'; import SearchBar from '@/pages/pegase/home/components/SearchBar'; import { useTranslation } from 'react-i18next'; import { RdsChip, RdsDivider } from 'rte-design-system-react'; +import { fetchProjectDetails } from '@/shared/services/projectService.ts'; const ProjectDetails = () => { const { t } = useTranslation(); @@ -34,42 +34,34 @@ const ProjectDetails = () => { searchStudy(userName); } }; - const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); + const [projectInfo, setProjectDetails] = useState({} as ProjectInfo); const location = useLocation(); const { projectId } = location.state || {}; useEffect(() => { - if (projectId && !projectInfo.id) { - const fetchProjectDetails = async () => { - try { - const response = await fetch(`${BASE_URL}/v1/project/${projectId}`); - - if (!response.ok) { - throw new Error('Failed to fetch project details'); - } + const getProjectDetails = async (projectId: string) => { + try { + const data = await fetchProjectDetails(projectId); - const data = await response.json(); - console.log('Fetched Data:', data); - - setProjectDetails({ - id: data.id, - name: data.name, - description: data.description, - createdBy: data.createdBy, - creationDate: data.creationDate, - archived: false, - pinned: false, - path: '', - tags: data.tags, - studies: [], - }); - } catch (error) { - console.error(`Error retrieving project details: ${projectId}`, error); - } - }; - - fetchProjectDetails(); + setProjectDetails({ + id: data.id, + name: data.name, + description: data.description, + createdBy: data.createdBy, + creationDate: data.creationDate, + archived: false, + pinned: false, + path: '', + tags: data.tags, + studies: [], + }); + } catch (error) { + console.error(`Error retrieving project details: ${projectId}`, error); + } + }; + if (projectId && !projectInfo.id) { + void getProjectDetails(projectId); } }, [projectId, projectInfo.id]); diff --git a/src/pages/pegase/projects/projectDetails/fetchProjectDetails.test.tsx b/src/pages/pegase/projects/projectDetails/fetchProjectDetails.test.tsx deleted file mode 100644 index e36a778..0000000 --- a/src/pages/pegase/projects/projectDetails/fetchProjectDetails.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { vi } from 'vitest'; - -const mockFetch = vi.fn(); -global.fetch = mockFetch; - -vi.mock('@/envVariables', () => ({ - getEnvVariables: vi.fn(() => 'https://mockapi.com'), -})); - -async function fetchProjectDetails(projectId: string, setProjectDetails: (details: any) => void) { - try { - const response = await fetch(`https://mockapi.com/v1/project/${projectId}`); - if (!response.ok) { - throw new Error('Failed to fetch project details'); - } - const data = await response.json(); - setProjectDetails({ - id: data.id, - name: data.name, - description: data.description, - createdBy: data.createdBy, - creationDate: data.creationDate, - archived: false, - pinned: false, - path: '', - tags: data.tags, - }); - } catch (error) { - console.error(`Error retrieving project details: ${projectId}`, error); - } -} - -describe('fetchProjectDetails', () => { - const mockSetProjectDetails = vi.fn(); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should fetch project details and call setProjectDetails', async () => { - const projectId = '123'; - const mockResponse = { - id: '123', - name: 'Project Name', - description: 'Project Description', - createdBy: 'User A', - creationDate: '2024-01-01', - tags: ['tag1', 'tag2'], - }; - - //Successful fetch response mock - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => mockResponse, - }); - - await fetchProjectDetails(projectId, mockSetProjectDetails); - - expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`); - expect(mockSetProjectDetails).toHaveBeenCalledWith({ - id: '123', - name: 'Project Name', - description: 'Project Description', - createdBy: 'User A', - creationDate: '2024-01-01', - archived: false, - pinned: false, - path: '', - tags: ['tag1', 'tag2'], - }); - }); - - it('should handle fetch failure gracefully', async () => { - const projectId = '123'; - - // Failed fetch response moc - mockFetch.mockResolvedValueOnce({ - ok: false, - }); - - await fetchProjectDetails(projectId, mockSetProjectDetails); - - expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`); - expect(mockSetProjectDetails).not.toHaveBeenCalled(); - }); - - it('should handle exceptions during fetch', async () => { - const projectId = '123'; - - //Fetch throwing an error mock - mockFetch.mockRejectedValueOnce(new Error('Network error')); - - await fetchProjectDetails(projectId, mockSetProjectDetails); - - expect(mockFetch).toHaveBeenCalledTimes(1); - expect(mockFetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`); - expect(mockSetProjectDetails).not.toHaveBeenCalled(); - }); -}); diff --git a/src/pages/pegase/projects/projectService.test.tsx b/src/pages/pegase/projects/projectService.test.tsx deleted file mode 100644 index ea668bb..0000000 --- a/src/pages/pegase/projects/projectService.test.tsx +++ /dev/null @@ -1,127 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { renderHook, waitFor } from '@testing-library/react'; -import { notifyToast } from '@/shared/notification/notification'; -import { pinProject, useFetchProjects } from './projectService'; - -const mockFetch = vi.fn(); -global.fetch = mockFetch; -vi.mock('@/shared/notification/notification'); -vi.mock('@/envVariables', () => ({ - getEnvVariables: vi.fn(() => 'https://mockapi.com'), -})); - -describe('useFetchProjects', () => { - beforeEach(() => { - global.fetch = vi.fn(() => - Promise.resolve({ - json: () => - Promise.resolve({ - content: [ - { - projectId: '1', - name: 'Project 1', - tags: ['Tag1', 'Tag2'], - creationDate: '2023-10-01', - createdBy: 'User A', - }, - ], - totalElements: 1, - }), - }), - ) as unknown as typeof fetch; - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('fetches projects on mount', async () => { - const { result } = renderHook(() => useFetchProjects('', 0, 9)); - - await waitFor(() => { - expect(result.current.projects).toEqual([ - { - projectId: '1', - name: 'Project 1', - tags: ['Tag1', 'Tag2'], - creationDate: '2023-10-01', - createdBy: 'User A', - }, - ]); - expect(result.current.count).toBe(1); - }); - }); - - it('fetches projects with search term', async () => { - renderHook(() => useFetchProjects('test', 0, 9)); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=1&size=9&search=test'); - }); - }); - - it('fetches projects with pagination', async () => { - renderHook(() => useFetchProjects('', 1, 9)); - - await waitFor(() => { - expect(global.fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/search?page=2&size=9&search='); - }); - }); -}); - -describe('pinProject', () => { - const mockIsReloadPinnedProject = vi.fn(); - const projectId = 'test-project-id'; - - beforeEach(() => { - global.fetch = vi.fn(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('should successfully pin a project and call notifyToast with success', async () => { - const mockResponse = { ok: true }; // Simulate successful fetch response - (global.fetch as ReturnType).mockResolvedValueOnce(mockResponse); - - await pinProject(projectId, mockIsReloadPinnedProject); - - // Check fetch call - expect(fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/pin?userId=me00247&projectId=test-project-id', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - - // Verify notifyToast was called with success - expect(notifyToast).toHaveBeenCalledWith({ - type: 'success', - message: 'Project pinned successfully', - }); - - // Verify isReloadPinnedProject was called - expect(mockIsReloadPinnedProject).toHaveBeenCalledWith(true); - }); - - it('should handle fetch-level errors and call notifyToast with error', async () => { - const networkErrorMessage = 'Network error'; - (global.fetch as ReturnType).mockRejectedValueOnce(new Error(networkErrorMessage)); - - await pinProject(projectId, mockIsReloadPinnedProject); - - // Verify notifyToast was called with the network error - expect(notifyToast).toHaveBeenCalledWith({ - type: 'error', - message: networkErrorMessage, - }); - - // Verify isReloadPinnedProject was not called - expect(mockIsReloadPinnedProject).not.toHaveBeenCalled(); - }); -}); diff --git a/src/pages/pegase/projects/projectService.ts b/src/pages/pegase/projects/projectService.ts deleted file mode 100644 index 063a521..0000000 --- a/src/pages/pegase/projects/projectService.ts +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - */ - -import { notifyToast } from '@/shared/notification/notification'; -import { useEffect, useState } from 'react'; -import { ProjectInfo } from '@/shared/types/pegase/Project.type'; -import { getEnvVariables } from '@/envVariables'; - -export const pinProject = async (projectId: string, isReloadPinnedProject: (value: boolean) => void) => { - const userId = 'me00247'; - const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - - try { - const response = await fetch(`${BASE_URL}/v1/project/pin?userId=${userId}&projectId=${projectId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - const errorData = JSON.parse(errorText); - throw new Error(`${errorData.message || errorText}`); - } - notifyToast({ - type: 'success', - message: 'Project pinned successfully', - }); - isReloadPinnedProject(true); - } catch (error: any) { - notifyToast({ - type: 'error', - message: `${error.message}`, - }); - } -}; - -export const useFetchProjects = (searchTerm: string, current: number, intervalSize: number) => { - const [projects, setProjects] = useState([]); - const [count, setCount] = useState(0); - const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - - const fetchProjects = () => { - const url = `${BASE_URL}/v1/project/search?page=${current + 1}&size=${intervalSize}&search=${searchTerm || ''}`; - fetch(url) - .then((response) => response.json()) - .then((json) => { - setProjects(json.content); - setCount(json.totalElements); - }) - .catch((error) => console.error(error)); - }; - - useEffect(() => { - fetchProjects(); - }, [BASE_URL, current, searchTerm, intervalSize]); - - return { projects, count, refetch: fetchProjects }; -}; - -export const deleteProjectById = async (projectId: string, isReloadProjects: (value: boolean) => void) => { - const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); - - try { - const response = await fetch(`${BASE_URL}/v1/project/${projectId}`, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - const errorData = JSON.parse(errorText); - throw new Error(`${errorData.message || errorText}`); - } - notifyToast({ - type: 'success', - message: 'Project deleted successfully', - }); - isReloadProjects(true); - } catch (error: any) { - notifyToast({ - type: 'error', - message: `${error.message}`, - }); - } -}; diff --git a/src/pages/pegase/settings/Settings.tsx b/src/pages/pegase/settings/Settings.tsx index 11f2719..b1666e2 100644 --- a/src/pages/pegase/settings/Settings.tsx +++ b/src/pages/pegase/settings/Settings.tsx @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { UserContext } from '@/contexts/UserContext'; +import { UserContext } from '@/store/contexts/UserContext'; import { THEME_COLOR } from '@/shared/types'; import i18next from 'i18next'; import { useTranslation } from 'react-i18next'; diff --git a/src/pages/pegase/studies/StudyCreationModal.tsx b/src/pages/pegase/studies/StudyCreationModal.tsx index ef409e6..8eaea9e 100644 --- a/src/pages/pegase/studies/StudyCreationModal.tsx +++ b/src/pages/pegase/studies/StudyCreationModal.tsx @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { RdsButton, RdsIconId, RdsInputText, RdsModal } from 'rte-design-system-react'; import { useTranslation } from 'react-i18next'; import KeywordsInput from '@/pages/pegase/studies/KeywordsInput'; @@ -14,7 +14,7 @@ import { saveStudy } from '@/pages/pegase/home/components/studyService'; import { StudyDTO } from '@/shared/types/index'; interface StudyCreationModalProps { - isOpen: boolean; + isOpen?: boolean; onClose: () => void; study?: StudyDTO | null; setReloadStudies: React.Dispatch>; diff --git a/src/routes.tsx b/src/routes.tsx index 2a0f562..576dced 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -11,7 +11,7 @@ import { StdIconId } from './shared/utils/common/mappings/iconMaps'; const Settings = lazy(() => import('./pages/pegase/settings/Settings')); const HomePage = lazy(() => import('./pages/pegase/home/HomePage')); -const ProjectsPage = lazy(() => import('./pages/pegase/projects/Projects')); +const ProjectsPage = lazy(() => import('./pages/pegase/projects/ProjectsPage')); const LogsPage = lazy(() => import('./pages/pegase/reports/LogsPage')); const AntaresPage = lazy(() => import('./pages/pegase/antares/Antares')); const LogoutPage = lazy(() => import('./pages/pegase/logout/Logout')); diff --git a/src/shared/const/apiEndPoint.ts b/src/shared/const/apiEndPoint.ts new file mode 100644 index 0000000..1094c10 --- /dev/null +++ b/src/shared/const/apiEndPoint.ts @@ -0,0 +1,20 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { getEnvVariables } from '@/envVariables.ts'; + +const BASE_URL = getEnvVariables('VITE_BACK_END_BASE_URL'); + +// STUDY +export const STUDY_SEARCH_ENDPOINT = `${BASE_URL}/v1/study/search`; + +// PINNED PROJECT +export const PROJECT_PINNED_ENDPOINT = `${BASE_URL}/v1/project/pinned`; +export const PROJECT_UNPIN_ENDPOINT = `${BASE_URL}/v1/project/unpin`; +export const PROJECT_PIN_PROJECT = `${BASE_URL}/v1/project/pin`; + +// PROJECT +export const PROJECT_ENDPOINT = `${BASE_URL}/v1/project`; diff --git a/src/hooks/common/useStdId.ts b/src/shared/enum/project.ts similarity index 55% rename from src/hooks/common/useStdId.ts rename to src/shared/enum/project.ts index 3b2d1eb..41bcb59 100644 --- a/src/hooks/common/useStdId.ts +++ b/src/shared/enum/project.ts @@ -4,9 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { useId } from 'react'; - -export const useStdId = (prefix: string, id?: string): string => { - const reactId = useId(); - return id || `${prefix}-${reactId}`; -}; +export enum PINNED_PROJECT_ACTION { + ADD_ITEM = 'ADD_ITEM', + REMOVE_ITEM = 'REMOVE_ITEM', + INIT_LIST = 'INIT_LIST', +} diff --git a/src/shared/services/pinnedProjectService.ts b/src/shared/services/pinnedProjectService.ts new file mode 100644 index 0000000..9366f7b --- /dev/null +++ b/src/shared/services/pinnedProjectService.ts @@ -0,0 +1,81 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { PROJECT_PIN_PROJECT, PROJECT_PINNED_ENDPOINT, PROJECT_UNPIN_ENDPOINT } from '@/shared/const/apiEndPoint'; +import { ProjectInfo } from '@/shared/types/pegase/Project.type'; + +/** + * Retrieve pinned projects list by user id + * + * @param {string} userId - User id + * @returns {Promise} - Promise object that represents a list of projects + */ +export const fetchPinnedProjects = async (userId: string) => { + const apiUrl = `${PROJECT_PINNED_ENDPOINT}?userId=${userId}`; + + const response = await fetch(apiUrl); + + if (!response?.ok) { + throw new Error('Failed to fetch project details'); + } + + const json = await response.json(); + return json.map((project: Partial) => ({ + ...project, + projectId: project.id?.toString(), + pinned: project.pinned ?? true, + })); +}; + +/** + * Handles the pin action. Displays a toast if the API call is successful. + * The API call to the /unpin endpoint is made only if the "Cancel" button + * on the toast is not clicked. + * + * @param {string} projectId - Project id + * @return {Promise} - Object that describes a project + */ + +export const pinProject = async (projectId: string) => { + const userId = 'me00247'; + const apiUrl = `${PROJECT_PIN_PROJECT}?userId=${userId}&projectId=${projectId}`; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`${errorText}`); + } + + return await response.json(); +}; + +/** + * Remove pinned project from the pinned project list + * + * @param {string} userId + * @param {string} projectId + */ +export const unpinProject = async (userId: string, projectId: string) => { + const apiUrl = `${PROJECT_UNPIN_ENDPOINT}?userId=${userId}&projectId=${projectId}`; + + const response = await fetch(apiUrl, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`${errorText}`); + } +}; diff --git a/src/shared/services/projectService.ts b/src/shared/services/projectService.ts new file mode 100644 index 0000000..486a930 --- /dev/null +++ b/src/shared/services/projectService.ts @@ -0,0 +1,38 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { PROJECT_ENDPOINT } from '@/shared/const/apiEndPoint'; + +export const deleteProjectById = async (projectId: string) => { + const response = await fetch(`${PROJECT_ENDPOINT}/${projectId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + const errorData = JSON.parse(errorText); + throw new Error(`${errorData.message || errorText}`); + } +}; + +/** + * Retrieve details of a project + * + * @param {string} projectId - Project id + * @return {Promise} - Project details + */ +export const fetchProjectDetails = async (projectId: string) => { + const response = await fetch(`${PROJECT_ENDPOINT}/${projectId}`); + + if (!response?.ok) { + throw new Error('Failed to fetch project details'); + } + + return await response.json(); +}; diff --git a/src/shared/services/studyService.ts b/src/shared/services/studyService.ts new file mode 100644 index 0000000..636721c --- /dev/null +++ b/src/shared/services/studyService.ts @@ -0,0 +1,42 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { PaginatedResponse, StudyDTO } from '@/shared/types'; +import { STUDY_SEARCH_ENDPOINT } from '@/shared/const/apiEndPoint'; + +/** + * Retrieve a list of studies from a term + * + * @param {string} searchTerm - Search term (ex: a user name) + * @param {string} projectId - Project id related to a study + * @param {number} currentPage - Current page number + * @param {number} intervalSize - Number of items per page + * @param {{ [key: string]: 'asc' | 'desc' })} sortBy - Object that describes the sorting type (ascending or descending) of a column + * + * @returns {Promise>} - Promise object that represents a list of studies + */ +export const fetchSearchStudies = async ( + searchTerm = '', + projectId = '', + currentPage = 0, + intervalSize = 0, + sortBy?: { [key: string]: 'asc' | 'desc' }, +): Promise> => { + let entries: [string, 'asc' | 'desc'] | null = null; + if (sortBy && JSON.stringify(sortBy) !== '{}') { + entries = Object.entries(sortBy)[0]; + } + + const apiUrl = `${STUDY_SEARCH_ENDPOINT}?page=${currentPage + 1}&size=${intervalSize}&projectId=${projectId}&search=${searchTerm}&sortColumn=${entries?.[0] ?? ''}&sortDirection=${entries?.[1] ?? ''}`; + + const response = await fetch(apiUrl); + if (!response.ok) { + throw new Error('Failed to fetch user studies'); + } + const json: PaginatedResponse = await response.json(); + + return { content: json.content, totalElements: json.totalElements }; +}; diff --git a/src/shared/services/test/pinnedProjectService.test.tsx b/src/shared/services/test/pinnedProjectService.test.tsx new file mode 100644 index 0000000..d9010bb --- /dev/null +++ b/src/shared/services/test/pinnedProjectService.test.tsx @@ -0,0 +1,170 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { fetchPinnedProjects, pinProject, unpinProject } from '../pinnedProjectService'; +import { vi } from 'vitest'; +import { waitFor } from '@testing-library/react'; + +const mockFetch = vi.fn(); +global.fetch = mockFetch; +vi.mock('@/shared/notification/notification'); +vi.mock('@/envVariables', () => ({ + getEnvVariables: vi.fn(() => 'https://mockapi.com'), +})); + +describe('pinProject', () => { + const projectId = 'test-project-id'; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should successfully pin a project and call notifyToast with success', async () => { + const mockResponse = { + id: '123', + name: 'Project Name', + description: 'Project Description', + createdBy: 'User A', + creationDate: '2024-01-01', + tags: ['tag1', 'tag2'], + }; + const mockResponseApi = { ok: true, json: async () => mockResponse }; // Simulate successful fetch response + (global.fetch as ReturnType).mockResolvedValueOnce(mockResponseApi); + + const response = await pinProject(projectId); + + // Check fetch call + expect(fetch).toHaveBeenCalledWith('https://mockapi.com/v1/project/pin?userId=me00247&projectId=test-project-id', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + expect(response).toEqual(mockResponse); + }); + + it('should handle pin project error ', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: async () => 'Error message', + }); + + await expect(async () => pinProject(projectId)).rejects.toThrowError('Error message'); + }); +}); + +describe('fetchPinnedProjects', () => { + const userId = '123'; + const mockResponse = [ + { + id: '123', + projectId: '123', + name: 'Project Name', + description: 'Project Description', + createdBy: 'User A', + creationDate: '2024-01-01', + tags: ['tag1', 'tag2'], + archived: true, + pinned: true, + path: '', + studies: [1, 2], + }, + { + id: '124', + projectId: '124', + name: 'Project Name 3', + description: 'Project Description', + createdBy: 'User A', + creationDate: '2024-01-01', + tags: ['tag1', 'tag2'], + archived: true, + pinned: true, + path: '', + studies: [1, 2], + }, + ]; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should fetch pinned project list', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + const result = await fetchPinnedProjects(userId); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/pinned?userId=${userId}`); + expect(result).toEqual(mockResponse); + }); + }); + + it('should handle fetch failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + }); + + await expect(async () => fetchPinnedProjects(userId)).rejects.toThrowError('Failed to fetch project details'); + }); +}); + +describe('unpinProject', () => { + const userId = '123'; + const projectId = 'project-id'; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should unpin project from pinned project list', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + }); + + await unpinProject(userId, projectId); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + `https://mockapi.com/v1/project/unpin?userId=${userId}&projectId=${projectId}`, + { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + }); + }); + + it('should handle fetch failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: async () => 'Error message', + }); + + await expect(async () => unpinProject(userId, projectId)).rejects.toThrowError('Error message'); + }); +}); diff --git a/src/shared/services/test/projectService.test.tsx b/src/shared/services/test/projectService.test.tsx new file mode 100644 index 0000000..580681c --- /dev/null +++ b/src/shared/services/test/projectService.test.tsx @@ -0,0 +1,110 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { deleteProjectById, fetchProjectDetails } from '../projectService.ts'; +import { vi } from 'vitest'; +import { waitFor } from '@testing-library/react'; + +const mockFetch = vi.fn(); +global.fetch = mockFetch; +vi.mock('@/shared/notification/notification'); +vi.mock('@/envVariables', () => ({ + getEnvVariables: vi.fn(() => 'https://mockapi.com'), +})); + +describe('deleteProjectById', () => { + const projectId = '123'; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should delete a pinned project from pinned project list', async () => { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + }); + + await deleteProjectById(projectId); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + }); + + it('should handle delete failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + text: async () => 'Failed to delete project', + }); + vi.stubGlobal('JSON', { parse: (text: string) => text }); + + await expect(async () => deleteProjectById(projectId)).rejects.toThrowError('Failed to delete project'); + }); +}); + +describe('fetchProjectDetails', () => { + const projectId = '123'; + const mockResponse = { + id: '123', + name: 'Project Name', + description: 'Project Description', + createdBy: 'User A', + creationDate: '2024-01-01', + tags: ['tag1', 'tag2'], + }; + + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should fetch project details', async () => { + //Successful fetch response mock + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + await fetchProjectDetails(projectId); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith(`https://mockapi.com/v1/project/${projectId}`); + }); + }); + + it('should handle fetch failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + }); + + await expect(async () => fetchProjectDetails(projectId)).rejects.toThrowError('Failed to fetch project details'); + }); + + it('should handle exceptions during fetch', async () => { + //Fetch throwing an error mock + global.fetch = vi.fn().mockRejectedValueOnce(new Error('Network error')); + + await expect(async () => fetchProjectDetails(projectId)).rejects.toThrowError('Network error'); + }); +}); diff --git a/src/shared/services/test/studyService.test.tsx b/src/shared/services/test/studyService.test.tsx new file mode 100644 index 0000000..581ef51 --- /dev/null +++ b/src/shared/services/test/studyService.test.tsx @@ -0,0 +1,76 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { vi } from 'vitest'; +import { waitFor } from '@testing-library/react'; +import { fetchSearchStudies } from '@/shared/services/studyService.ts'; + +vi.mock('@/envVariables', () => ({ + getEnvVariables: vi.fn(() => 'https://mockapi.com'), +})); + +describe('fetchSearchStudies', () => { + beforeEach(() => { + global.fetch = vi.fn(); + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should fetch study list', async () => { + //Successful fetch response mock + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + json: () => + Promise.resolve({ + content: [ + { + projectId: '1', + name: 'Project 1', + tags: ['Tag1', 'Tag2'], + creationDate: '2023-10-01', + createdBy: 'User A', + }, + ], + totalElements: 1, + }), + }); + + const result = await fetchSearchStudies('test', '124', 3, 10, { column: 'asc' }); + + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + `https://mockapi.com/v1/study/search?page=4&size=10&projectId=124&search=test&sortColumn=column&sortDirection=asc`, + ); + expect(result).toEqual({ + content: [ + { + projectId: '1', + name: 'Project 1', + tags: ['Tag1', 'Tag2'], + creationDate: '2023-10-01', + createdBy: 'User A', + }, + ], + totalElements: 1, + }); + }); + }); + + it('should handle fetch failure gracefully', async () => { + // Failed fetch response moc + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + }); + + await expect(async () => fetchSearchStudies('test', '124', 3, 10, { column: 'asc' })).rejects.toThrowError( + 'Failed to fetch user studies', + ); + }); +}); diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 8a5231f..6c5b85a 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -9,4 +9,4 @@ export * from './common/StdBase.type'; export * from './common/Tailwind.type'; export * from './common/User.type'; export * from './common/UserSettings.type'; -export * from './pegase/study'; +export * from './pegase/Study.type'; diff --git a/src/shared/types/pegase/Project.type.ts b/src/shared/types/pegase/Project.type.ts index 79eca44..8540169 100644 --- a/src/shared/types/pegase/Project.type.ts +++ b/src/shared/types/pegase/Project.type.ts @@ -4,6 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; + export interface ProjectInfo { id: string; name: string; @@ -16,3 +18,18 @@ export interface ProjectInfo { tags: string[]; studies: number[]; } + +export type PinnedProjectActionType = + | { type: PINNED_PROJECT_ACTION.ADD_ITEM; payload: ProjectInfo } + | { + type: PINNED_PROJECT_ACTION.REMOVE_ITEM; + payload: string; + } + | { + type: PINNED_PROJECT_ACTION.INIT_LIST; + payload: ProjectInfo[]; + }; + +export interface PinnedProjectState { + pinnedProjects: ProjectInfo[]; +} diff --git a/src/shared/types/pegase/study.ts b/src/shared/types/pegase/Study.type.ts similarity index 77% rename from src/shared/types/pegase/study.ts rename to src/shared/types/pegase/Study.type.ts index 27e5462..f2346d4 100644 --- a/src/shared/types/pegase/study.ts +++ b/src/shared/types/pegase/Study.type.ts @@ -4,7 +4,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -export type StudyDTO = { +export interface StudyDTO { id: number; name: string; createdBy: string; @@ -14,4 +14,9 @@ export type StudyDTO = { status: string; horizon: string; trajectoryIds: number[]; -}; +} + +export interface PaginatedResponse { + content: T[]; + totalElements: number; +} diff --git a/src/contexts/modalContext.ts b/src/store/contexts/ModalContext.tsx similarity index 100% rename from src/contexts/modalContext.ts rename to src/store/contexts/ModalContext.tsx diff --git a/src/store/contexts/ProjectContext.tsx b/src/store/contexts/ProjectContext.tsx new file mode 100644 index 0000000..934df7a --- /dev/null +++ b/src/store/contexts/ProjectContext.tsx @@ -0,0 +1,34 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react'; +import { PinnedProjectActionType, PinnedProjectState } from '@/shared/types/pegase/Project.type'; +import pinnedProjectReducer from '@/store/reducers/projectReducer'; + +const initialValue: PinnedProjectState = { pinnedProjects: [] }; + +export const PinnedProjectContext = createContext(initialValue); +export const PinnedProjectDispatchContext = createContext | null>(null); + +export const usePinnedProject = () => useContext(PinnedProjectContext); +export const usePinnedProjectDispatch = () => useContext(PinnedProjectDispatchContext); + +export interface PinnedProjectProviderProps { + children: ReactNode; + initialValue: PinnedProjectState; +} + +export const PinnedProjectProvider = ({ children, initialValue }: PinnedProjectProviderProps) => { + const initializer = (value = initialValue) => value; + + const [state, dispatch] = useReducer(pinnedProjectReducer, initialValue, initializer); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/UserContext.tsx b/src/store/contexts/UserContext.tsx similarity index 100% rename from src/contexts/UserContext.tsx rename to src/store/contexts/UserContext.tsx diff --git a/src/contexts/createFastContext.tsx b/src/store/contexts/createFastContext.tsx similarity index 93% rename from src/contexts/createFastContext.tsx rename to src/store/contexts/createFastContext.tsx index 8b58548..989389b 100644 --- a/src/contexts/createFastContext.tsx +++ b/src/store/contexts/createFastContext.tsx @@ -4,8 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Draft, create } from 'mutative'; -import React, { createContext, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from 'react'; +import { create, Draft } from 'mutative'; +import { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useSyncExternalStore } from 'react'; export type StoreOption = { addOnlyIfAbsent?: boolean; @@ -91,7 +91,7 @@ export default function createFastContext> const StoreContext = createContext(null); - function Provider({ initialState, children }: { initialState: Store; children: React.ReactNode }) { + function Provider({ initialState, children }: Readonly<{ initialState: Store; children: ReactNode }>) { return {children}; } diff --git a/src/store/reducers/projectReducer.tsx b/src/store/reducers/projectReducer.tsx new file mode 100644 index 0000000..d27f283 --- /dev/null +++ b/src/store/reducers/projectReducer.tsx @@ -0,0 +1,35 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { PinnedProjectActionType, PinnedProjectState, ProjectInfo } from '@/shared/types/pegase/Project.type'; +import { PINNED_PROJECT_ACTION } from '@/shared/enum/project.ts'; + +const addItem = (currentState: ProjectInfo[], payload: ProjectInfo) => { + return { pinnedProjects: [...currentState, payload] }; +}; +const removeItem = (currentState: ProjectInfo[], payload: string) => { + return { pinnedProjects: [...currentState.filter((p) => p.id !== payload)] }; +}; + +const pinnedProjectReducer = (prevState: PinnedProjectState, action?: PinnedProjectActionType): PinnedProjectState => { + const { pinnedProjects } = prevState; + if (action) { + switch (action.type) { + case PINNED_PROJECT_ACTION.ADD_ITEM: + return addItem(pinnedProjects, action.payload); + case PINNED_PROJECT_ACTION.REMOVE_ITEM: + return removeItem(pinnedProjects, action.payload); + case PINNED_PROJECT_ACTION.INIT_LIST: + return { pinnedProjects: [...action.payload] }; + default: + return prevState; + } + } + + return prevState; +}; + +export default pinnedProjectReducer; diff --git a/src/testSetup.ts b/src/testSetup.ts index a2d6828..b66f136 100644 --- a/src/testSetup.ts +++ b/src/testSetup.ts @@ -13,7 +13,7 @@ beforeEach(async () => { await i18n.init(); }); -// runs a cleanup adter each test case (e.g. clearing jsdom) +// runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { cleanup(); }); diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo index f13cd3b..9b90ff6 100644 --- a/tsconfig.tsbuildinfo +++ b/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/envVariables.ts","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/testSetup.ts","./src/vite-env.d.ts","./src/components/common/base/stdIcon/Icon.tsx","./src/components/common/base/stdIcon/StdIcon.tsx","./src/components/common/base/stdIcon/iconClassBuilder.ts","./src/components/common/data/stdSimpleTable/StdSimpleTable.tsx","./src/components/common/data/stdSimpleTable/tests/stdSimpleTable.test.tsx","./src/components/common/data/stdTable/TableContext.tsx","./src/components/common/data/stdTable/TableCore.tsx","./src/components/common/data/stdTable/tableCoreRowClassBuilder.ts","./src/components/common/data/stdTable/useTableContext.ts","./src/components/common/data/stdTable/cells/ExpandableCell.tsx","./src/components/common/data/stdTable/cells/tests/expandableCell.test.tsx","./src/components/common/data/stdTable/features/readOnly.ts","./src/components/common/data/stdTable/lineRender/StdCollapseIcon.tsx","./src/components/common/data/stdTable/tests/TableCore.test.tsx","./src/components/common/data/stdTable/tests/tableCoreRowClassBuilder.test.ts","./src/components/common/data/stdTable/tests/testTableUtils.ts","./src/components/common/data/stdTable/types/readOnly.type.d.ts","./src/components/common/data/stdTable/types/sizeClassNames.d.ts","./src/components/common/handler/ThemeHandler.tsx","./src/components/common/handler/test/ThemeHandler.test.tsx","./src/components/common/layout/stdAvatar/StdAvatar.tsx","./src/components/common/layout/stdAvatar/avatarClassBuilder.ts","./src/components/common/layout/stdAvatar/tests/StdAvatar.test.tsx","./src/components/common/layout/stdAvatar/tests/avatarClassBuilder.test.ts","./src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx","./src/components/common/layout/stdAvatarGroup/avatarGroupClassBuilder.ts","./src/components/common/layout/stdAvatarGroup/avatarTools.ts","./src/components/common/layout/stdAvatarGroup/tests/StdAvatarGroup.test.tsx","./src/components/common/layout/stdAvatarGroup/tests/avatarTools.test.ts","./src/components/common/layout/stdNavbar/StdNavbar.tsx","./src/components/common/layout/stdNavbar/StdNavbarController.tsx","./src/components/common/layout/stdNavbar/StdNavbarHeader.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenu.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx","./src/components/common/layout/stdNavbar/navbarClassBuilder.ts","./src/components/common/layout/stdNavbar/tests/StdNavbar.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarController.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarHeader.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarMenuItem.test.tsx","./src/components/common/layout/stdNavbar/tests/navbarClassBuilder.test.ts","./src/components/common/layout/stdTextTooltip/StdTextTooltip.tsx","./src/components/common/layout/stdTextTooltip/tests/stdTooltipText.test.tsx","./src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx","./src/components/pegase/header/Header.tsx","./src/components/pegase/navbar/Navbar.tsx","./src/components/pegase/pegaseCard/cardClassBuilder.ts","./src/components/pegase/pegaseCard/pegaseCard.tsx","./src/components/pegase/pegaseCard/useDropdownOptions.ts","./src/components/pegase/pegaseCard/pegaseCardTitle/cardTitleClassBuilder.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/pegaseCardTitle.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/cardTitleClassBuilder.test.ts","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/pegaseCardTitle.test.tsx","./src/components/pegase/pegaseCard/tests/cardTripleActionClassBuilder.test.ts","./src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx","./src/components/pegase/star/PegaseStar.tsx","./src/contexts/UserContext.tsx","./src/contexts/createFastContext.tsx","./src/contexts/modalContext.ts","./src/hooks/useDateFormatter.ts","./src/hooks/useFocusTrapping.ts","./src/hooks/useInputFormState.ts","./src/hooks/useModal.ts","./src/hooks/useProjectNavigation.ts","./src/hooks/useStdId.ts","./src/hooks/common/useActiveKeyboard.ts","./src/hooks/common/useCallOnResize.ts","./src/hooks/common/useDebounce.ts","./src/hooks/common/useInputFormState.ts","./src/hooks/common/usePrevious.ts","./src/hooks/common/useStdId.ts","./src/hooks/common/useTimeout.ts","./src/hooks/common/test/useCallOnResize.test.ts","./src/hooks/common/test/useDebounce.test.ts","./src/hooks/common/test/useInputFormState.test.ts","./src/hooks/common/test/usePrevious.test.ts","./src/hooks/common/test/useStdId.test.ts","./src/hooks/common/test/useTimeout.test.ts","./src/mocks/mockTools.ts","./src/mocks/data/components/dropdownItems.mock.ts","./src/mocks/data/components/navbarHeader.ts","./src/mocks/data/features/menuItemData.mock.tsx","./src/mocks/data/list/keywords.ts","./src/mocks/data/list/names.ts","./src/mocks/data/list/projectName.ts","./src/mocks/data/list/studyName.ts","./src/mocks/data/list/user.mocks.ts","./src/mocks/data/list/user.ts","./src/pages/pegase/antares/Antares.tsx","./src/pages/pegase/home/HomePage.tsx","./src/pages/pegase/home/components/HomePageContent.tsx","./src/pages/pegase/home/components/SearchBar.tsx","./src/pages/pegase/home/components/StudiesPagination.tsx","./src/pages/pegase/home/components/StudyTableDisplay.tsx","./src/pages/pegase/home/components/StudyTableHeaders.tsx","./src/pages/pegase/home/components/StudyTableUtils.tsx","./src/pages/pegase/home/components/studyService.ts","./src/pages/pegase/home/components/useStudyTableDisplay.ts","./src/pages/pegase/home/components/tests/useStudyTableDisplay.test.ts","./src/pages/pegase/home/pinnedProjects/PinnedProject.tsx","./src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx","./src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx","./src/pages/pegase/home/pinnedProjects/tests/handleUnpin.test.ts","./src/pages/pegase/logout/Logout.tsx","./src/pages/pegase/projects/ProjectContent.tsx","./src/pages/pegase/projects/Projects.tsx","./src/pages/pegase/projects/ProjectsPagination.tsx","./src/pages/pegase/projects/projectService.test.tsx","./src/pages/pegase/projects/projectService.ts","./src/pages/pegase/projects/projectDetails/ProjectDetails.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsContent.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx","./src/pages/pegase/projects/projectDetails/fetchProjectDetails.test.tsx","./src/pages/pegase/reports/LogsPage.tsx","./src/pages/pegase/settings/Settings.tsx","./src/pages/pegase/studies/HorizonInput.tsx","./src/pages/pegase/studies/KeywordsInput.tsx","./src/pages/pegase/studies/ProjectInput.tsx","./src/pages/pegase/studies/StudyCreationModal.tsx","./src/pages/pegase/studies/useStudyNavigation.ts","./src/pages/pegase/studies/studyDetails/AreaLinkTab.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx","./src/pages/pegase/studies/studyDetails/EnrTab.tsx","./src/pages/pegase/studies/studyDetails/LoadTab.tsx","./src/pages/pegase/studies/studyDetails/MiscLinkTab.tsx","./src/pages/pegase/studies/studyDetails/StudyDetailsContent.tsx","./src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx","./src/pages/pegase/studies/studyDetails/ThermalTab.tsx","./src/pages/pegase/studies/studyDetails/areaLinkTable.tsx","./src/pages/pegase/studies/studyDetails/studyDetails.tsx","./src/pages/pegase/studies/studyDetails/studyHeader.tsx","./src/shared/constants.ts","./src/shared/notification/containers.tsx","./src/shared/notification/notification.tsx","./src/shared/types/index.ts","./src/shared/types/common/DisplayStatus.type.ts","./src/shared/types/common/MenuNavItem.type.ts","./src/shared/types/common/StdBase.type.ts","./src/shared/types/common/StudyStatus.type.ts","./src/shared/types/common/Tailwind.type.ts","./src/shared/types/common/TailwindColorClass.type.ts","./src/shared/types/common/User.type.ts","./src/shared/types/common/UserSettings.type.ts","./src/shared/types/common/tests/testUtils.tsx","./src/shared/types/pegase/Project.type.ts","./src/shared/types/pegase/study.ts","./src/shared/utils/dateFormatter.ts","./src/shared/utils/slotsUtils.ts","./src/shared/utils/tabIndexUtils.ts","./src/shared/utils/common/defaultUtils.ts","./src/shared/utils/common/displayUtils.ts","./src/shared/utils/common/slotsUtils.ts","./src/shared/utils/common/classes/classMerger.ts","./src/shared/utils/common/classes/test/classMerger.test.ts","./src/shared/utils/common/dom/getDimensions.ts","./src/shared/utils/common/dom/test/getDimensions.test.tsx","./src/shared/utils/common/mappings/iconMaps.ts","./tailwind.config.ts","./vite-env.d.ts"],"version":"5.7.2"} \ No newline at end of file +{"root":["./src/App.tsx","./src/envVariables.ts","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/testSetup.ts","./src/vite-env.d.ts","./src/components/common/base/stdIcon/Icon.tsx","./src/components/common/base/stdIcon/StdIcon.tsx","./src/components/common/base/stdIcon/iconClassBuilder.ts","./src/components/common/data/stdSimpleTable/StdSimpleTable.tsx","./src/components/common/data/stdSimpleTable/tests/stdSimpleTable.test.tsx","./src/components/common/data/stdTable/TableContext.tsx","./src/components/common/data/stdTable/TableCore.tsx","./src/components/common/data/stdTable/tableCoreRowClassBuilder.ts","./src/components/common/data/stdTable/useTableContext.ts","./src/components/common/data/stdTable/cells/ExpandableCell.tsx","./src/components/common/data/stdTable/cells/tests/expandableCell.test.tsx","./src/components/common/data/stdTable/features/readOnly.ts","./src/components/common/data/stdTable/lineRender/StdCollapseIcon.tsx","./src/components/common/data/stdTable/tests/TableCore.test.tsx","./src/components/common/data/stdTable/tests/tableCoreRowClassBuilder.test.ts","./src/components/common/data/stdTable/tests/testTableUtils.ts","./src/components/common/data/stdTable/types/readOnly.type.d.ts","./src/components/common/data/stdTable/types/sizeClassNames.d.ts","./src/components/common/handler/ThemeHandler.tsx","./src/components/common/handler/test/ThemeHandler.test.tsx","./src/components/common/layout/stdAvatar/StdAvatar.tsx","./src/components/common/layout/stdAvatar/avatarClassBuilder.ts","./src/components/common/layout/stdAvatar/tests/StdAvatar.test.tsx","./src/components/common/layout/stdAvatar/tests/avatarClassBuilder.test.ts","./src/components/common/layout/stdAvatarGroup/StdAvatarGroup.tsx","./src/components/common/layout/stdAvatarGroup/avatarGroupClassBuilder.ts","./src/components/common/layout/stdAvatarGroup/avatarTools.ts","./src/components/common/layout/stdAvatarGroup/tests/StdAvatarGroup.test.tsx","./src/components/common/layout/stdAvatarGroup/tests/avatarTools.test.ts","./src/components/common/layout/stdNavbar/StdNavbar.tsx","./src/components/common/layout/stdNavbar/StdNavbarController.tsx","./src/components/common/layout/stdNavbar/StdNavbarHeader.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenu.tsx","./src/components/common/layout/stdNavbar/StdNavbarMenuItem.tsx","./src/components/common/layout/stdNavbar/navbarClassBuilder.ts","./src/components/common/layout/stdNavbar/tests/StdNavbar.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarController.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarHeader.test.tsx","./src/components/common/layout/stdNavbar/tests/StdNavbarMenuItem.test.tsx","./src/components/common/layout/stdNavbar/tests/navbarClassBuilder.test.ts","./src/components/common/layout/stdTextWithTooltip/StdTextWithTooltip.tsx","./src/components/pegase/header/Header.tsx","./src/components/pegase/navbar/Navbar.tsx","./src/components/pegase/pegaseCard/cardClassBuilder.ts","./src/components/pegase/pegaseCard/pegaseCard.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/cardTitleClassBuilder.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/pegaseCardTitle.tsx","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/cardTitleClassBuilder.test.ts","./src/components/pegase/pegaseCard/pegaseCardTitle/tests/pegaseCardTitle.test.tsx","./src/components/pegase/pegaseCard/tests/cardTripleActionClassBuilder.test.ts","./src/components/pegase/pegaseCard/tests/pegaseCardTripleAction.test.tsx","./src/components/pegase/star/PegaseStar.tsx","./src/hooks/useDateFormatter.ts","./src/hooks/useDropdownOptions.ts","./src/hooks/useFetchProjectList.ts","./src/hooks/useHandlePinnedProjectList.ts","./src/hooks/useNewStudyModal.ts","./src/hooks/useProjectNavigation.ts","./src/hooks/useStudyNavigation.ts","./src/hooks/useStudyTableDisplay.ts","./src/hooks/common/usePrevious.ts","./src/hooks/common/test/usePrevious.test.ts","./src/hooks/test/useDropdownOptions.test.ts","./src/hooks/test/useFetchProjectList.test.ts","./src/hooks/test/useHandlePinnedProjectList.test.tsx","./src/hooks/test/useNewStudyModal.test.ts","./src/hooks/test/useProjectNavigation.test.tsx","./src/hooks/test/useStudyNavigation.test.tsx","./src/hooks/test/useStudyTableDisplay.test.ts","./src/mocks/mockTools.ts","./src/mocks/data/components/dropdownItems.mock.ts","./src/mocks/data/components/navbarHeader.ts","./src/mocks/data/features/menuItemData.mock.tsx","./src/mocks/data/list/keywords.ts","./src/mocks/data/list/names.ts","./src/mocks/data/list/projectName.ts","./src/mocks/data/list/studyName.ts","./src/mocks/data/list/user.mocks.ts","./src/mocks/data/list/user.ts","./src/pages/pegase/antares/Antares.tsx","./src/pages/pegase/home/HomePage.tsx","./src/pages/pegase/home/components/HomePageContent.tsx","./src/pages/pegase/home/components/SearchBar.tsx","./src/pages/pegase/home/components/StudiesPagination.tsx","./src/pages/pegase/home/components/StudyTableDisplay.tsx","./src/pages/pegase/home/components/StudyTableHeaders.tsx","./src/pages/pegase/home/components/StudyTableUtils.tsx","./src/pages/pegase/home/components/studyService.ts","./src/pages/pegase/home/pinnedProjects/PinnedProject.tsx","./src/pages/pegase/home/pinnedProjects/PinnedProjectCard.tsx","./src/pages/pegase/home/pinnedProjects/ProjectCreator.tsx","./src/pages/pegase/logout/Logout.tsx","./src/pages/pegase/projects/ProjectContent.tsx","./src/pages/pegase/projects/ProjectsPage.tsx","./src/pages/pegase/projects/ProjectsPagination.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetails.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsContent.tsx","./src/pages/pegase/projects/projectDetails/ProjectDetailsHeader.tsx","./src/pages/pegase/reports/LogsPage.tsx","./src/pages/pegase/settings/Settings.tsx","./src/pages/pegase/studies/HorizonInput.tsx","./src/pages/pegase/studies/KeywordsInput.tsx","./src/pages/pegase/studies/ProjectInput.tsx","./src/pages/pegase/studies/StudyCreationModal.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTab.tsx","./src/pages/pegase/studies/studyDetails/AreaLinkTableHeaders.tsx","./src/pages/pegase/studies/studyDetails/EnrTab.tsx","./src/pages/pegase/studies/studyDetails/LoadTab.tsx","./src/pages/pegase/studies/studyDetails/MiscLinkTab.tsx","./src/pages/pegase/studies/studyDetails/StudyDetailsContent.tsx","./src/pages/pegase/studies/studyDetails/StudyNavigationMenu.tsx","./src/pages/pegase/studies/studyDetails/ThermalTab.tsx","./src/pages/pegase/studies/studyDetails/areaLinkTable.tsx","./src/pages/pegase/studies/studyDetails/studyDetails.tsx","./src/pages/pegase/studies/studyDetails/studyHeader.tsx","./src/shared/constants.ts","./src/shared/const/apiEndPoint.ts","./src/shared/enum/project.ts","./src/shared/notification/containers.tsx","./src/shared/notification/notification.tsx","./src/shared/services/pinnedProjectService.ts","./src/shared/services/projectService.ts","./src/shared/services/studyService.ts","./src/shared/services/test/pinnedProjectService.test.tsx","./src/shared/services/test/projectService.test.tsx","./src/shared/services/test/studyService.test.tsx","./src/shared/types/index.ts","./src/shared/types/common/DisplayStatus.type.ts","./src/shared/types/common/MenuNavItem.type.ts","./src/shared/types/common/StdBase.type.ts","./src/shared/types/common/StudyStatus.type.ts","./src/shared/types/common/Tailwind.type.ts","./src/shared/types/common/TailwindColorClass.type.ts","./src/shared/types/common/User.type.ts","./src/shared/types/common/UserSettings.type.ts","./src/shared/types/common/tests/testUtils.tsx","./src/shared/types/pegase/Project.type.ts","./src/shared/types/pegase/Study.type.ts","./src/shared/utils/dateFormatter.ts","./src/shared/utils/slotsUtils.ts","./src/shared/utils/tabIndexUtils.ts","./src/shared/utils/common/defaultUtils.ts","./src/shared/utils/common/displayUtils.ts","./src/shared/utils/common/slotsUtils.ts","./src/shared/utils/common/classes/classMerger.ts","./src/shared/utils/common/classes/test/classMerger.test.ts","./src/shared/utils/common/dom/getDimensions.ts","./src/shared/utils/common/dom/test/getDimensions.test.tsx","./src/shared/utils/common/mappings/iconMaps.ts","./src/store/contexts/ModalContext.tsx","./src/store/contexts/ProjectContext.tsx","./src/store/contexts/UserContext.tsx","./src/store/contexts/createFastContext.tsx","./src/store/reducers/projectReducer.tsx","./tailwind.config.ts","./vite-env.d.ts"],"version":"5.7.2"} \ No newline at end of file